smart_domain 0.1.0 → 0.1.2
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/.rubocop.yml +65 -0
- data/CHANGELOG.md +6 -8
- data/README.md +52 -0
- data/Rakefile +3 -3
- data/lib/generators/{active_domain → smart_domain}/domain/domain_generator.rb +16 -16
- data/lib/generators/{active_domain → smart_domain}/domain/templates/events/created_event.rb.tt +1 -1
- data/lib/generators/{active_domain → smart_domain}/domain/templates/events/deleted_event.rb.tt +1 -1
- data/lib/generators/{active_domain → smart_domain}/domain/templates/events/updated_event.rb.tt +2 -2
- data/lib/generators/{active_domain → smart_domain}/domain/templates/service.rb.tt +5 -5
- data/lib/generators/{active_domain → smart_domain}/domain/templates/setup.rb.tt +4 -4
- data/lib/generators/{active_domain → smart_domain}/install/install_generator.rb +13 -13
- data/lib/smart_domain/configuration.rb +1 -1
- data/lib/smart_domain/domain/exceptions.rb +2 -2
- data/lib/smart_domain/domain/service.rb +1 -1
- data/lib/smart_domain/event/adapters/memory.rb +24 -9
- data/lib/smart_domain/event/base.rb +26 -13
- data/lib/smart_domain/event/handler.rb +5 -3
- data/lib/smart_domain/event/mixins.rb +11 -1
- data/lib/smart_domain/event/registration.rb +4 -4
- data/lib/smart_domain/generators/domain_generator.rb +1 -1
- data/lib/smart_domain/generators/install_generator.rb +1 -1
- data/lib/smart_domain/handlers/audit_handler.rb +22 -24
- data/lib/smart_domain/handlers/metrics_handler.rb +8 -1
- data/lib/smart_domain/integration/active_record.rb +6 -8
- data/lib/smart_domain/railtie.rb +24 -24
- data/lib/smart_domain/tasks/domains.rake +18 -14
- data/lib/smart_domain/version.rb +1 -1
- data/lib/smart_domain.rb +20 -20
- data/smart_domain.gemspec +26 -25
- metadata +32 -43
- data/examples/blog_app/.kamal/hooks/docker-setup.sample +0 -3
- data/examples/blog_app/.kamal/hooks/post-app-boot.sample +0 -3
- data/examples/blog_app/.kamal/hooks/post-deploy.sample +0 -14
- data/examples/blog_app/.kamal/hooks/post-proxy-reboot.sample +0 -3
- data/examples/blog_app/.kamal/hooks/pre-app-boot.sample +0 -3
- data/examples/blog_app/.kamal/hooks/pre-build.sample +0 -51
- data/examples/blog_app/.kamal/hooks/pre-connect.sample +0 -47
- data/examples/blog_app/.kamal/hooks/pre-deploy.sample +0 -122
- data/examples/blog_app/.kamal/hooks/pre-proxy-reboot.sample +0 -3
- data/examples/blog_app/.kamal/secrets +0 -20
- data/examples/blog_app/bin/kamal +0 -27
- data/examples/blog_app/config/deploy.yml +0 -120
- data/examples/blog_app/config/master.key +0 -1
- /data/examples/blog_app/config/initializers/{active_domain.rb → smart_domain.rb} +0 -0
- /data/lib/generators/{active_domain → smart_domain}/domain/templates/policy.rb.tt +0 -0
- /data/lib/generators/{active_domain → smart_domain}/install/templates/README +0 -0
- /data/lib/generators/{active_domain → smart_domain}/install/templates/application_event.rb +0 -0
- /data/lib/generators/{active_domain → smart_domain}/install/templates/application_policy.rb +0 -0
- /data/lib/generators/{active_domain → smart_domain}/install/templates/application_service.rb +0 -0
- /data/lib/generators/{active_domain → smart_domain}/install/templates/initializer.rb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc4986884824f0ebda3f3ea7051e7303bb416b7497f1b3213a749bdbd2a80918
|
|
4
|
+
data.tar.gz: 5a65d52f74b31c7ebc0da5d8a63d8b8942b5262746de39826f185d61129f32e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06015b7d5dc2598a35fa0ebda62d2c2ece2b1c7a1cfc4719713d70e54fdebde7b713c84f713334853d9244d9f994b88d6a4937cc6b7dcedb25f8800bd7d87551
|
|
7
|
+
data.tar.gz: 2af6c6ee30a91e21947907885f4662b3565576b9a53631eb216a799167e2ff88f5088bee7b6bf6c5e04c01b9062bec243518aee6f556eb023cda2b784d6f5ea5
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
NewCops: enable
|
|
3
|
+
TargetRubyVersion: 3.0
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
Exclude:
|
|
6
|
+
- 'vendor/**/*'
|
|
7
|
+
- 'bin/**/*'
|
|
8
|
+
- 'node_modules/**/*'
|
|
9
|
+
- 'examples/**/*'
|
|
10
|
+
|
|
11
|
+
# Allow longer lines in tests
|
|
12
|
+
Layout/LineLength:
|
|
13
|
+
Max: 120
|
|
14
|
+
Exclude:
|
|
15
|
+
- 'spec/**/*'
|
|
16
|
+
|
|
17
|
+
# Allow long blocks in specs and rake tasks
|
|
18
|
+
Metrics/BlockLength:
|
|
19
|
+
Exclude:
|
|
20
|
+
- 'spec/**/*'
|
|
21
|
+
- '*.gemspec'
|
|
22
|
+
- 'lib/**/*.rake'
|
|
23
|
+
|
|
24
|
+
# Allow longer methods for generators and complex logic
|
|
25
|
+
Metrics/MethodLength:
|
|
26
|
+
Max: 30
|
|
27
|
+
|
|
28
|
+
# Allow more complex methods
|
|
29
|
+
Metrics/AbcSize:
|
|
30
|
+
Max: 30
|
|
31
|
+
Exclude:
|
|
32
|
+
- 'lib/generators/**/*'
|
|
33
|
+
|
|
34
|
+
# Allow more cyclomatic complexity
|
|
35
|
+
Metrics/CyclomaticComplexity:
|
|
36
|
+
Max: 10
|
|
37
|
+
|
|
38
|
+
# Allow higher perceived complexity
|
|
39
|
+
Metrics/PerceivedComplexity:
|
|
40
|
+
Max: 10
|
|
41
|
+
|
|
42
|
+
# Allow longer classes for handlers
|
|
43
|
+
Metrics/ClassLength:
|
|
44
|
+
Max: 150
|
|
45
|
+
|
|
46
|
+
# Allow defining constants in test blocks
|
|
47
|
+
Lint/ConstantDefinitionInBlock:
|
|
48
|
+
Exclude:
|
|
49
|
+
- 'spec/**/*'
|
|
50
|
+
|
|
51
|
+
# Allow duplicate branches (false positives)
|
|
52
|
+
Lint/DuplicateBranch:
|
|
53
|
+
Enabled: false
|
|
54
|
+
|
|
55
|
+
# Allow development dependencies in gemspec
|
|
56
|
+
Gemspec/DevelopmentDependencies:
|
|
57
|
+
Enabled: false
|
|
58
|
+
|
|
59
|
+
# Allow more than one class per file in specs
|
|
60
|
+
Style/ClassAndModuleChildren:
|
|
61
|
+
Enabled: false
|
|
62
|
+
|
|
63
|
+
# Don't require documentation for all classes
|
|
64
|
+
Style/Documentation:
|
|
65
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
|
@@ -7,14 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- Rails generators for domain scaffolding
|
|
17
|
-
- Comprehensive documentation
|
|
10
|
+
## [0.1.1] - 2025-12-31
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Enhanced README with AI-augmented development advantages section
|
|
14
|
+
- Explained how DDD/EDA architecture reduces context windows by 93%
|
|
15
|
+
- Added concrete examples of reduced cognitive load for AI assistants
|
|
18
16
|
|
|
19
17
|
## [0.1.0] - 2025-12-29
|
|
20
18
|
|
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# SmartDomain
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/smart_domain)
|
|
4
|
+
[](https://rubygems.org/gems/smart_domain)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://www.ruby-lang.org)
|
|
7
|
+
|
|
3
8
|
**Domain-Driven Design and Event-Driven Architecture for Rails**
|
|
4
9
|
|
|
5
10
|
SmartDomain brings battle-tested DDD/EDA patterns from platform to Ruby on Rails applications. It provides domain events, an event bus, generic handlers, and Rails generators for rapid domain scaffolding.
|
|
@@ -15,6 +20,53 @@ SmartDomain brings battle-tested DDD/EDA patterns from platform to Ruby on Rails
|
|
|
15
20
|
- ✅ **Multi-tenancy Support** - Built-in support for multi-tenant applications
|
|
16
21
|
- ✅ **Audit Compliance** - Automatic audit logging for compliance requirements
|
|
17
22
|
|
|
23
|
+
## Why SmartDomain for AI-Augmented Development?
|
|
24
|
+
|
|
25
|
+
SmartDomain's architecture is uniquely suited for AI-augmented development workflows:
|
|
26
|
+
|
|
27
|
+
**Reduced Context Windows**
|
|
28
|
+
- Domain-driven design creates **loosely coupled bounded contexts**
|
|
29
|
+
- Each domain (User, Order, Product) is self-contained with its own services, events, and policies
|
|
30
|
+
- AI tools can focus on one domain at a time, drastically reducing context requirements
|
|
31
|
+
- Clear boundaries mean AI understands exactly what code is relevant
|
|
32
|
+
|
|
33
|
+
**Event-Driven Decoupling**
|
|
34
|
+
- Events decouple domains from each other
|
|
35
|
+
- Changes in one domain don't cascade through the codebase
|
|
36
|
+
- AI can modify a single domain without understanding the entire system
|
|
37
|
+
- Explicit event contracts make dependencies transparent
|
|
38
|
+
|
|
39
|
+
**Explicit Patterns**
|
|
40
|
+
- Standardized structure (Service → Events → Handlers) makes code predictable
|
|
41
|
+
- AI learns the pattern once, applies it everywhere
|
|
42
|
+
- Generators scaffold domains with consistent architecture
|
|
43
|
+
- Less cognitive load for both humans and AI
|
|
44
|
+
|
|
45
|
+
**Example: AI Working with SmartDomain**
|
|
46
|
+
|
|
47
|
+
When an AI needs to add a "suspend user" feature:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Traditional monolithic approach:
|
|
51
|
+
- AI must understand: User model, callbacks, mailers, notifications, audit logs,
|
|
52
|
+
related models, 15+ files across different layers
|
|
53
|
+
- Context window: ~3000 lines of code
|
|
54
|
+
|
|
55
|
+
SmartDomain approach:
|
|
56
|
+
- AI focuses on: UserService (150 lines), UserSuspendedEvent (20 lines)
|
|
57
|
+
- Events automatically trigger audit, metrics, emails via handlers
|
|
58
|
+
- Context window: ~200 lines of code
|
|
59
|
+
- 93% reduction in context requirements
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Benefits for Your Development Workflow**
|
|
63
|
+
- **Faster iterations** - AI assistants work with smaller, focused contexts
|
|
64
|
+
- **Better code quality** - Consistent patterns reduce hallucinations
|
|
65
|
+
- **Easier maintenance** - Clear boundaries make changes predictable
|
|
66
|
+
- **Natural collaboration** - AI and human developers work with the same mental model
|
|
67
|
+
|
|
68
|
+
SmartDomain isn't just better architecture—it's architecture optimized for the AI development era.
|
|
69
|
+
|
|
18
70
|
## Installation
|
|
19
71
|
|
|
20
72
|
Add this line to your application's Gemfile:
|
data/Rakefile
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
-
require
|
|
8
|
+
require 'rubocop/rake_task'
|
|
9
9
|
|
|
10
10
|
RuboCop::RakeTask.new
|
|
11
11
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/named_base'
|
|
5
5
|
|
|
6
6
|
module SmartDomain
|
|
7
7
|
module Generators
|
|
@@ -22,18 +22,18 @@ module SmartDomain
|
|
|
22
22
|
# user_updated_event.rb
|
|
23
23
|
# user_deleted_event.rb
|
|
24
24
|
class DomainGenerator < Rails::Generators::NamedBase
|
|
25
|
-
source_root File.expand_path(
|
|
25
|
+
source_root File.expand_path('templates', __dir__)
|
|
26
26
|
|
|
27
|
-
desc
|
|
27
|
+
desc 'Generate a complete domain structure with service, events, and policy'
|
|
28
28
|
|
|
29
29
|
class_option :skip_service, type: :boolean, default: false,
|
|
30
|
-
|
|
30
|
+
desc: 'Skip generating domain service'
|
|
31
31
|
class_option :skip_policy, type: :boolean, default: false,
|
|
32
|
-
|
|
32
|
+
desc: 'Skip generating domain policy'
|
|
33
33
|
class_option :skip_events, type: :boolean, default: false,
|
|
34
|
-
|
|
34
|
+
desc: 'Skip generating domain events'
|
|
35
35
|
class_option :skip_setup, type: :boolean, default: false,
|
|
36
|
-
|
|
36
|
+
desc: 'Skip generating setup file'
|
|
37
37
|
|
|
38
38
|
# Generate domain directory structure
|
|
39
39
|
def create_domain_directory
|
|
@@ -44,30 +44,30 @@ module SmartDomain
|
|
|
44
44
|
def create_service
|
|
45
45
|
return if options[:skip_service]
|
|
46
46
|
|
|
47
|
-
template
|
|
47
|
+
template 'service.rb.tt', "#{domain_path}/#{file_name}_service.rb"
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# Generate domain events in app/events/
|
|
51
51
|
def create_events
|
|
52
52
|
return if options[:skip_events]
|
|
53
53
|
|
|
54
|
-
template
|
|
55
|
-
template
|
|
56
|
-
template
|
|
54
|
+
template 'events/created_event.rb.tt', "app/events/#{file_name}_created_event.rb"
|
|
55
|
+
template 'events/updated_event.rb.tt', "app/events/#{file_name}_updated_event.rb"
|
|
56
|
+
template 'events/deleted_event.rb.tt', "app/events/#{file_name}_deleted_event.rb"
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
# Generate domain policy
|
|
60
60
|
def create_policy
|
|
61
61
|
return if options[:skip_policy]
|
|
62
62
|
|
|
63
|
-
template
|
|
63
|
+
template 'policy.rb.tt', "app/policies/#{file_name}_policy.rb"
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# Generate setup file for event registration
|
|
67
67
|
def create_setup
|
|
68
68
|
return if options[:skip_setup]
|
|
69
69
|
|
|
70
|
-
template
|
|
70
|
+
template 'setup.rb.tt', "#{domain_path}/setup.rb"
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Show instructions
|
|
@@ -84,10 +84,10 @@ module SmartDomain
|
|
|
84
84
|
say " #{domain_path}/setup.rb" unless options[:skip_setup]
|
|
85
85
|
|
|
86
86
|
say "\nNext steps:"
|
|
87
|
-
say
|
|
87
|
+
say ' 1. Review and customize the generated files'
|
|
88
88
|
say " 2. Add business logic to #{class_name}Service"
|
|
89
89
|
say " 3. Customize authorization rules in #{class_name}Policy"
|
|
90
|
-
say
|
|
90
|
+
say ' 4. Restart your Rails server to load the domain setup'
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
private
|
data/lib/generators/{active_domain → smart_domain}/domain/templates/events/updated_event.rb.tt
RENAMED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# Event published when a <%= file_name %> is updated
|
|
4
4
|
class <%= class_name %>UpdatedEvent < ApplicationEvent
|
|
5
|
-
include
|
|
6
|
-
include
|
|
5
|
+
include SmartDomain::Event::ActorMixin
|
|
6
|
+
include SmartDomain::Event::ChangeTrackingMixin
|
|
7
7
|
|
|
8
8
|
attribute :<%= file_name %>_id, :string
|
|
9
9
|
|
|
@@ -11,12 +11,12 @@ module <%= domain_module_name %>
|
|
|
11
11
|
#
|
|
12
12
|
# @param attributes [Hash] <%= class_name %> attributes
|
|
13
13
|
# @return [<%= class_name %>] Created <%= file_name %>
|
|
14
|
-
# @raise [
|
|
15
|
-
# @raise [
|
|
14
|
+
# @raise [SmartDomain::Domain::AlreadyExistsError] If <%= file_name %> already exists
|
|
15
|
+
# @raise [SmartDomain::Domain::ValidationError] If validation fails
|
|
16
16
|
def create_<%= file_name %>(attributes)
|
|
17
17
|
# Example business rule validation
|
|
18
18
|
# if <%= class_name %>.exists?(email: attributes[:email])
|
|
19
|
-
# raise
|
|
19
|
+
# raise SmartDomain::Domain::AlreadyExistsError.new('<%= class_name %>', 'email', attributes[:email])
|
|
20
20
|
# end
|
|
21
21
|
|
|
22
22
|
<%= class_name %>.transaction do
|
|
@@ -43,7 +43,7 @@ module <%= domain_module_name %>
|
|
|
43
43
|
# @param attributes [Hash] Attributes to update
|
|
44
44
|
# @return [<%= class_name %>] Updated <%= file_name %>
|
|
45
45
|
# @raise [ActiveRecord::RecordNotFound] If <%= file_name %> not found
|
|
46
|
-
# @raise [
|
|
46
|
+
# @raise [SmartDomain::Domain::UnauthorizedError] If not authorized
|
|
47
47
|
def update_<%= file_name %>(<%= file_name %>_id, attributes)
|
|
48
48
|
<%= file_name %> = <%= class_name %>.find(<%= file_name %>_id)
|
|
49
49
|
|
|
@@ -65,7 +65,7 @@ module <%= domain_module_name %>
|
|
|
65
65
|
# @param <%= file_name %>_id [Integer, String] <%= class_name %> ID
|
|
66
66
|
# @return [Boolean] True if deleted
|
|
67
67
|
# @raise [ActiveRecord::RecordNotFound] If <%= file_name %> not found
|
|
68
|
-
# @raise [
|
|
68
|
+
# @raise [SmartDomain::Domain::UnauthorizedError] If not authorized
|
|
69
69
|
def delete_<%= file_name %>(<%= file_name %>_id)
|
|
70
70
|
<%= file_name %> = <%= class_name %>.find(<%= file_name %>_id)
|
|
71
71
|
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
module <%= domain_module_name %>
|
|
4
4
|
# Setup event handlers for <%= file_name %> domain
|
|
5
5
|
#
|
|
6
|
-
# This file is automatically loaded by
|
|
6
|
+
# This file is automatically loaded by SmartDomain::Railtie
|
|
7
7
|
# when the Rails application starts.
|
|
8
8
|
def self.setup!
|
|
9
9
|
# Register standard handlers (audit and metrics)
|
|
10
10
|
# This one line replaces ~50 lines of boilerplate!
|
|
11
|
-
|
|
11
|
+
SmartDomain::Event::Registration.register_standard_handlers(
|
|
12
12
|
domain: '<%= file_name %>',
|
|
13
13
|
events: %w[created updated deleted],
|
|
14
14
|
include_audit: true,
|
|
@@ -19,8 +19,8 @@ module <%= domain_module_name %>
|
|
|
19
19
|
# Example:
|
|
20
20
|
#
|
|
21
21
|
# email_handler = <%= class_name %>EmailHandler.new
|
|
22
|
-
#
|
|
23
|
-
#
|
|
22
|
+
# SmartDomain::Event.bus.subscribe('<%= file_name %>.created', email_handler)
|
|
23
|
+
# SmartDomain::Event.bus.subscribe('<%= file_name %>.updated', email_handler)
|
|
24
24
|
|
|
25
25
|
Rails.logger.info "[<%= domain_module_name %>] Domain setup complete"
|
|
26
26
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'rails/generators'
|
|
4
4
|
|
|
5
5
|
module SmartDomain
|
|
6
6
|
module Generators
|
|
@@ -16,42 +16,42 @@ module SmartDomain
|
|
|
16
16
|
# - app/handlers/ directory
|
|
17
17
|
# - app/policies/ directory
|
|
18
18
|
class InstallGenerator < Rails::Generators::Base
|
|
19
|
-
source_root File.expand_path(
|
|
19
|
+
source_root File.expand_path('templates', __dir__)
|
|
20
20
|
|
|
21
|
-
desc
|
|
21
|
+
desc 'Install SmartDomain into your Rails application'
|
|
22
22
|
|
|
23
23
|
# Create initializer
|
|
24
24
|
def create_initializer
|
|
25
|
-
template
|
|
25
|
+
template 'initializer.rb', 'config/initializers/smart_domain.rb'
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Create directory structure
|
|
29
29
|
def create_directory_structure
|
|
30
|
-
create_file
|
|
31
|
-
create_file
|
|
32
|
-
create_file
|
|
33
|
-
create_file
|
|
34
|
-
create_file
|
|
30
|
+
create_file 'app/domains/.keep'
|
|
31
|
+
create_file 'app/events/.keep'
|
|
32
|
+
create_file 'app/handlers/.keep'
|
|
33
|
+
create_file 'app/policies/.keep'
|
|
34
|
+
create_file 'app/services/.keep'
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Create base event class
|
|
38
38
|
def create_application_event
|
|
39
|
-
template
|
|
39
|
+
template 'application_event.rb', 'app/events/application_event.rb'
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# Create base policy class
|
|
43
43
|
def create_application_policy
|
|
44
|
-
template
|
|
44
|
+
template 'application_policy.rb', 'app/policies/application_policy.rb'
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# Create base service class
|
|
48
48
|
def create_application_service
|
|
49
|
-
template
|
|
49
|
+
template 'application_service.rb', 'app/services/application_service.rb'
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# Show post-install message
|
|
53
53
|
def show_readme
|
|
54
|
-
readme
|
|
54
|
+
readme 'README' if behavior == :invoke
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
end
|
|
@@ -84,7 +84,7 @@ module SmartDomain
|
|
|
84
84
|
# )
|
|
85
85
|
class BusinessRuleError < Error
|
|
86
86
|
def initialize(message, code: :business_rule_violation, details: {})
|
|
87
|
-
super
|
|
87
|
+
super
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
|
|
@@ -134,7 +134,7 @@ module SmartDomain
|
|
|
134
134
|
# 'User does not have permission to delete this resource'
|
|
135
135
|
# )
|
|
136
136
|
class UnauthorizedError < Error
|
|
137
|
-
def initialize(message =
|
|
137
|
+
def initialize(message = 'Unauthorized', action: nil, resource: nil)
|
|
138
138
|
super(
|
|
139
139
|
message,
|
|
140
140
|
code: :unauthorized,
|
|
@@ -150,7 +150,7 @@ module SmartDomain
|
|
|
150
150
|
attributes[:organization_id] ||= current_organization_id
|
|
151
151
|
|
|
152
152
|
# Auto-fill actor fields if event includes ActorMixin
|
|
153
|
-
if event_class.
|
|
153
|
+
if event_class.method_defined?(:actor_id)
|
|
154
154
|
attributes[:actor_id] ||= current_user_id&.to_s
|
|
155
155
|
attributes[:actor_email] ||= current_user&.email
|
|
156
156
|
end
|
|
@@ -18,7 +18,10 @@ module SmartDomain
|
|
|
18
18
|
class Memory
|
|
19
19
|
def initialize
|
|
20
20
|
@handlers = Hash.new { |h, k| h[k] = [] }
|
|
21
|
-
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def logger
|
|
24
|
+
@logger ||= SmartDomain.configuration.logger
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
# Subscribe a handler to an event type
|
|
@@ -30,16 +33,30 @@ module SmartDomain
|
|
|
30
33
|
|
|
31
34
|
# Publish an event to all subscribed handlers
|
|
32
35
|
# @param event [SmartDomain::Event::Base] Event to publish
|
|
36
|
+
# @raise [ValidationError] If event is invalid
|
|
33
37
|
def publish(event)
|
|
38
|
+
# Validate event before publishing
|
|
39
|
+
unless event.is_a?(SmartDomain::Event::Base)
|
|
40
|
+
raise SmartDomain::Event::ValidationError, 'Event must be a SmartDomain::Event::Base'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
unless event.valid?
|
|
45
|
+
raise SmartDomain::Event::ValidationError, "Invalid event: #{event.errors.full_messages.join(', ')}"
|
|
46
|
+
end
|
|
47
|
+
rescue NoMethodError => e
|
|
48
|
+
raise SmartDomain::Event::ValidationError, "Malformed event: #{e.message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
34
51
|
# Find all matching handlers (exact match + wildcard patterns)
|
|
35
52
|
matching_handlers = find_matching_handlers(event.event_type)
|
|
36
53
|
|
|
37
54
|
if matching_handlers.empty?
|
|
38
|
-
|
|
55
|
+
logger.debug "[SmartDomain::Memory] No handlers for event type: #{event.event_type}"
|
|
39
56
|
return
|
|
40
57
|
end
|
|
41
58
|
|
|
42
|
-
|
|
59
|
+
logger.debug "[SmartDomain::Memory] Notifying #{matching_handlers.size} handler(s) for #{event.event_type}"
|
|
43
60
|
|
|
44
61
|
matching_handlers.each do |handler|
|
|
45
62
|
handle_event(handler, event)
|
|
@@ -69,9 +86,7 @@ module SmartDomain
|
|
|
69
86
|
handlers = []
|
|
70
87
|
|
|
71
88
|
@handlers.each do |pattern, pattern_handlers|
|
|
72
|
-
if event_type_matches?(event_type, pattern)
|
|
73
|
-
handlers.concat(pattern_handlers)
|
|
74
|
-
end
|
|
89
|
+
handlers.concat(pattern_handlers) if event_type_matches?(event_type, pattern)
|
|
75
90
|
end
|
|
76
91
|
|
|
77
92
|
handlers.uniq
|
|
@@ -86,7 +101,7 @@ module SmartDomain
|
|
|
86
101
|
return true if event_type == pattern
|
|
87
102
|
|
|
88
103
|
# Wildcard pattern match (e.g., "user.*" matches "user.created")
|
|
89
|
-
if pattern.end_with?(
|
|
104
|
+
if pattern.end_with?('.*')
|
|
90
105
|
prefix = pattern[0..-3] # Remove ".*"
|
|
91
106
|
return event_type.start_with?("#{prefix}.")
|
|
92
107
|
end
|
|
@@ -100,8 +115,8 @@ module SmartDomain
|
|
|
100
115
|
def handle_event(handler, event)
|
|
101
116
|
handler.handle(event)
|
|
102
117
|
rescue StandardError => e
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
logger.error "[SmartDomain::Memory] Error in event handler #{handler.class.name}: #{e.message}"
|
|
119
|
+
logger.error e.backtrace.join("\n")
|
|
105
120
|
# Swallow exception to not affect other handlers
|
|
106
121
|
end
|
|
107
122
|
end
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
3
|
+
require 'active_model'
|
|
4
|
+
require 'active_support/core_ext/object/blank'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
require 'logger'
|
|
7
7
|
|
|
8
8
|
module SmartDomain
|
|
9
9
|
module Event
|
|
10
|
+
# Validation error for domain events
|
|
11
|
+
class ValidationError < StandardError; end
|
|
12
|
+
|
|
10
13
|
# Base class for all domain events in the system.
|
|
11
14
|
#
|
|
12
15
|
# Domain events represent significant business occurrences that other
|
|
@@ -58,7 +61,9 @@ module SmartDomain
|
|
|
58
61
|
def initialize(attributes = {})
|
|
59
62
|
super
|
|
60
63
|
freeze_event
|
|
61
|
-
|
|
64
|
+
return if valid?
|
|
65
|
+
|
|
66
|
+
raise ValidationError, "Event validation failed: #{errors.full_messages.join(', ')}"
|
|
62
67
|
end
|
|
63
68
|
|
|
64
69
|
# Convert event to hash
|
|
@@ -101,13 +106,16 @@ module SmartDomain
|
|
|
101
106
|
# event = UserCreatedEvent.new(...)
|
|
102
107
|
# bus.publish(event)
|
|
103
108
|
class Bus
|
|
104
|
-
attr_reader :adapter
|
|
109
|
+
attr_reader :adapter
|
|
105
110
|
|
|
106
111
|
# Initialize event bus with optional adapter
|
|
107
112
|
# @param adapter [Object, Symbol] Event bus adapter or adapter name (default: Memory adapter)
|
|
108
113
|
def initialize(adapter: nil)
|
|
109
114
|
@adapter = resolve_adapter(adapter)
|
|
110
|
-
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def logger
|
|
118
|
+
@logger ||= SmartDomain.configuration.logger
|
|
111
119
|
end
|
|
112
120
|
|
|
113
121
|
# Subscribe a handler to a specific event type
|
|
@@ -115,7 +123,7 @@ module SmartDomain
|
|
|
115
123
|
# @param handler [Object] Handler object that responds to #handle(event)
|
|
116
124
|
def subscribe(event_type, handler)
|
|
117
125
|
@adapter.subscribe(event_type, handler)
|
|
118
|
-
|
|
126
|
+
logger.info "[SmartDomain] Event handler subscribed: #{handler.class.name} -> #{event_type}"
|
|
119
127
|
end
|
|
120
128
|
|
|
121
129
|
# Publish an event to all registered handlers
|
|
@@ -124,8 +132,8 @@ module SmartDomain
|
|
|
124
132
|
def publish(event)
|
|
125
133
|
validate_event!(event)
|
|
126
134
|
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
logger.info "[SmartDomain] Publishing event: #{event.event_type} (#{event.event_id})"
|
|
136
|
+
logger.debug "[SmartDomain] Event details: #{event.to_h}"
|
|
129
137
|
|
|
130
138
|
@adapter.publish(event)
|
|
131
139
|
end
|
|
@@ -149,15 +157,20 @@ module SmartDomain
|
|
|
149
157
|
|
|
150
158
|
# Validate event before publishing
|
|
151
159
|
# @param event [Object] Event to validate
|
|
152
|
-
# @raise [ArgumentError] If event is not
|
|
160
|
+
# @raise [ArgumentError] If event is not a Base instance
|
|
161
|
+
# @raise [ValidationError] If event validation fails
|
|
153
162
|
def validate_event!(event)
|
|
154
163
|
unless event.is_a?(Base)
|
|
155
164
|
raise ArgumentError, "Event must be a SmartDomain::Event::Base, got #{event.class.name}"
|
|
156
165
|
end
|
|
157
166
|
|
|
158
|
-
|
|
167
|
+
begin
|
|
168
|
+
return if event.valid?
|
|
159
169
|
|
|
160
|
-
|
|
170
|
+
raise ValidationError, "Event validation failed: #{event.errors.full_messages.join(', ')}"
|
|
171
|
+
rescue NoMethodError => e
|
|
172
|
+
raise ValidationError, "Malformed event: #{e.message}"
|
|
173
|
+
end
|
|
161
174
|
end
|
|
162
175
|
end
|
|
163
176
|
|