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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +65 -0
  3. data/CHANGELOG.md +6 -8
  4. data/README.md +52 -0
  5. data/Rakefile +3 -3
  6. data/lib/generators/{active_domain → smart_domain}/domain/domain_generator.rb +16 -16
  7. data/lib/generators/{active_domain → smart_domain}/domain/templates/events/created_event.rb.tt +1 -1
  8. data/lib/generators/{active_domain → smart_domain}/domain/templates/events/deleted_event.rb.tt +1 -1
  9. data/lib/generators/{active_domain → smart_domain}/domain/templates/events/updated_event.rb.tt +2 -2
  10. data/lib/generators/{active_domain → smart_domain}/domain/templates/service.rb.tt +5 -5
  11. data/lib/generators/{active_domain → smart_domain}/domain/templates/setup.rb.tt +4 -4
  12. data/lib/generators/{active_domain → smart_domain}/install/install_generator.rb +13 -13
  13. data/lib/smart_domain/configuration.rb +1 -1
  14. data/lib/smart_domain/domain/exceptions.rb +2 -2
  15. data/lib/smart_domain/domain/service.rb +1 -1
  16. data/lib/smart_domain/event/adapters/memory.rb +24 -9
  17. data/lib/smart_domain/event/base.rb +26 -13
  18. data/lib/smart_domain/event/handler.rb +5 -3
  19. data/lib/smart_domain/event/mixins.rb +11 -1
  20. data/lib/smart_domain/event/registration.rb +4 -4
  21. data/lib/smart_domain/generators/domain_generator.rb +1 -1
  22. data/lib/smart_domain/generators/install_generator.rb +1 -1
  23. data/lib/smart_domain/handlers/audit_handler.rb +22 -24
  24. data/lib/smart_domain/handlers/metrics_handler.rb +8 -1
  25. data/lib/smart_domain/integration/active_record.rb +6 -8
  26. data/lib/smart_domain/railtie.rb +24 -24
  27. data/lib/smart_domain/tasks/domains.rake +18 -14
  28. data/lib/smart_domain/version.rb +1 -1
  29. data/lib/smart_domain.rb +20 -20
  30. data/smart_domain.gemspec +26 -25
  31. metadata +32 -43
  32. data/examples/blog_app/.kamal/hooks/docker-setup.sample +0 -3
  33. data/examples/blog_app/.kamal/hooks/post-app-boot.sample +0 -3
  34. data/examples/blog_app/.kamal/hooks/post-deploy.sample +0 -14
  35. data/examples/blog_app/.kamal/hooks/post-proxy-reboot.sample +0 -3
  36. data/examples/blog_app/.kamal/hooks/pre-app-boot.sample +0 -3
  37. data/examples/blog_app/.kamal/hooks/pre-build.sample +0 -51
  38. data/examples/blog_app/.kamal/hooks/pre-connect.sample +0 -47
  39. data/examples/blog_app/.kamal/hooks/pre-deploy.sample +0 -122
  40. data/examples/blog_app/.kamal/hooks/pre-proxy-reboot.sample +0 -3
  41. data/examples/blog_app/.kamal/secrets +0 -20
  42. data/examples/blog_app/bin/kamal +0 -27
  43. data/examples/blog_app/config/deploy.yml +0 -120
  44. data/examples/blog_app/config/master.key +0 -1
  45. /data/examples/blog_app/config/initializers/{active_domain.rb → smart_domain.rb} +0 -0
  46. /data/lib/generators/{active_domain → smart_domain}/domain/templates/policy.rb.tt +0 -0
  47. /data/lib/generators/{active_domain → smart_domain}/install/templates/README +0 -0
  48. /data/lib/generators/{active_domain → smart_domain}/install/templates/application_event.rb +0 -0
  49. /data/lib/generators/{active_domain → smart_domain}/install/templates/application_policy.rb +0 -0
  50. /data/lib/generators/{active_domain → smart_domain}/install/templates/application_service.rb +0 -0
  51. /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: e5e5a4574b90ca06fb1e2747b2da47b7a5168794f4e80901ee2688930f2e6ad8
4
- data.tar.gz: e1ada977da14eb9f8ec6092dcfee612f7283847d4a36b136faeca0dc4d51b92a
3
+ metadata.gz: dc4986884824f0ebda3f3ea7051e7303bb416b7497f1b3213a749bdbd2a80918
4
+ data.tar.gz: 5a65d52f74b31c7ebc0da5d8a63d8b8942b5262746de39826f185d61129f32e7
5
5
  SHA512:
6
- metadata.gz: 03d5fe8c7ad6be23ee69b9d11139ade594130151d12d7ea30f68156c90fa1dc05c1e133da427bc71e0a852eab0455a5c4d2d11f16345c05397b2856842898961
7
- data.tar.gz: 9aeb909c122d04cf2677aceb63d7f2b43b9b1a7d86c92264e9a4096cd1ee69470501c1e1e57b082fe73d6d48dbc5d4e40195d12df7a68116c12a73e0cf74a00c
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
- ### Added
11
- - Initial gem structure
12
- - Event system (DomainEvent, EventBus, Event Mixins)
13
- - Generic handlers (Audit, Metrics)
14
- - Domain service pattern
15
- - ActiveRecord integration
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
+ [![Gem Version](https://badge.fury.io/rb/smart_domain.svg)](https://badge.fury.io/rb/smart_domain)
4
+ [![Downloads](https://img.shields.io/gem/dt/smart_domain.svg)](https://rubygems.org/gems/smart_domain)
5
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0-ruby.svg)](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 "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "rubocop/rake_task"
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 "rails/generators"
4
- require "rails/generators/named_base"
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("templates", __dir__)
25
+ source_root File.expand_path('templates', __dir__)
26
26
 
27
- desc "Generate a complete domain structure with service, events, and policy"
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
- desc: "Skip generating domain service"
30
+ desc: 'Skip generating domain service'
31
31
  class_option :skip_policy, type: :boolean, default: false,
32
- desc: "Skip generating domain policy"
32
+ desc: 'Skip generating domain policy'
33
33
  class_option :skip_events, type: :boolean, default: false,
34
- desc: "Skip generating domain events"
34
+ desc: 'Skip generating domain events'
35
35
  class_option :skip_setup, type: :boolean, default: false,
36
- desc: "Skip generating setup file"
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 "service.rb.tt", "#{domain_path}/#{file_name}_service.rb"
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 "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"
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 "policy.rb.tt", "app/policies/#{file_name}_policy.rb"
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 "setup.rb.tt", "#{domain_path}/setup.rb"
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 " 1. Review and customize the generated files"
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 " 4. Restart your Rails server to load the domain setup"
90
+ say ' 4. Restart your Rails server to load the domain setup'
91
91
  end
92
92
 
93
93
  private
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Event published when a <%= file_name %> is created
4
4
  class <%= class_name %>CreatedEvent < ApplicationEvent
5
- include ActiveDomain::Event::ActorMixin
5
+ include SmartDomain::Event::ActorMixin
6
6
 
7
7
  attribute :<%= file_name %>_id, :string
8
8
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Event published when a <%= file_name %> is deleted
4
4
  class <%= class_name %>DeletedEvent < ApplicationEvent
5
- include ActiveDomain::Event::ActorMixin
5
+ include SmartDomain::Event::ActorMixin
6
6
 
7
7
  attribute :<%= file_name %>_id, :string
8
8
 
@@ -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 ActiveDomain::Event::ActorMixin
6
- include ActiveDomain::Event::ChangeTrackingMixin
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 [ActiveDomain::Domain::AlreadyExistsError] If <%= file_name %> already exists
15
- # @raise [ActiveDomain::Domain::ValidationError] If validation fails
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 ActiveDomain::Domain::AlreadyExistsError.new('<%= class_name %>', 'email', attributes[:email])
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 [ActiveDomain::Domain::UnauthorizedError] If not authorized
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 [ActiveDomain::Domain::UnauthorizedError] If not authorized
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 ActiveDomain::Railtie
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
- ActiveDomain::Event::Registration.register_standard_handlers(
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
- # ActiveDomain::Event.bus.subscribe('<%= file_name %>.created', email_handler)
23
- # ActiveDomain::Event.bus.subscribe('<%= file_name %>.updated', email_handler)
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 "rails/generators"
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("templates", __dir__)
19
+ source_root File.expand_path('templates', __dir__)
20
20
 
21
- desc "Install SmartDomain into your Rails application"
21
+ desc 'Install SmartDomain into your Rails application'
22
22
 
23
23
  # Create initializer
24
24
  def create_initializer
25
- template "initializer.rb", "config/initializers/smart_domain.rb"
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 "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"
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 "application_event.rb", "app/events/application_event.rb"
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 "application_policy.rb", "app/policies/application_policy.rb"
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 "application_service.rb", "app/services/application_service.rb"
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 "README" if behavior == :invoke
54
+ readme 'README' if behavior == :invoke
55
55
  end
56
56
  end
57
57
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "logger"
3
+ require 'logger'
4
4
 
5
5
  module SmartDomain
6
6
  # Configuration for SmartDomain gem.
@@ -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(message, code: code, details: details)
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 = "Unauthorized", action: nil, resource: nil)
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.instance_methods.include?(:actor_id)
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
- @logger = ActiveSupport::TaggedLogging.new(Logger.new($stdout))
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
- @logger.debug "[SmartDomain::Memory] No handlers for event type: #{event.event_type}"
55
+ logger.debug "[SmartDomain::Memory] No handlers for event type: #{event.event_type}"
39
56
  return
40
57
  end
41
58
 
42
- @logger.debug "[SmartDomain::Memory] Notifying #{matching_handlers.size} handler(s) for #{event.event_type}"
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
- @logger.error "[SmartDomain::Memory] Handler #{handler.class.name} failed: #{e.message}"
104
- @logger.error e.backtrace.join("\n")
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 "active_model"
4
- require "active_support/core_ext/object/blank"
5
- require "securerandom"
6
- require "logger"
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
- validate!
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, :logger
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
- @logger = ActiveSupport::TaggedLogging.new(Logger.new($stdout))
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
- @logger.info "[SmartDomain] Event handler subscribed: #{handler.class.name} -> #{event_type}"
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
- @logger.info "[SmartDomain] Publishing event: #{event.event_type} (#{event.event_id})"
128
- @logger.debug "[SmartDomain] Event details: #{event.to_h}"
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 valid
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
- return if event.valid?
167
+ begin
168
+ return if event.valid?
159
169
 
160
- raise ArgumentError, "Event validation failed: #{event.errors.full_messages.join(', ')}"
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