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,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= domain_module_name %>
4
+ # Service for <%= class_name %> business logic
5
+ #
6
+ # This service encapsulates all business operations for <%= plural_name %>.
7
+ # Controllers should delegate to this service rather than directly
8
+ # manipulating <%= class_name %> models.
9
+ class <%= class_name %>Service < ApplicationService
10
+ # Create a new <%= file_name %>
11
+ #
12
+ # @param attributes [Hash] <%= class_name %> attributes
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
16
+ def create_<%= file_name %>(attributes)
17
+ # Example business rule validation
18
+ # if <%= class_name %>.exists?(email: attributes[:email])
19
+ # raise ActiveDomain::Domain::AlreadyExistsError.new('<%= class_name %>', 'email', attributes[:email])
20
+ # end
21
+
22
+ <%= class_name %>.transaction do
23
+ <%= file_name %> = <%= class_name %>.create!(attributes)
24
+
25
+ # Event is published via ActiveRecord integration
26
+ # Or manually:
27
+ # event = build_event(<%= class_name %>CreatedEvent,
28
+ # event_type: '<%= file_name %>.created',
29
+ # aggregate_id: <%= file_name %>.id,
30
+ # aggregate_type: '<%= class_name %>',
31
+ # <%= file_name %>_id: <%= file_name %>.id
32
+ # )
33
+ # publish_after_commit(event)
34
+
35
+ log(:info, "<%= class_name %> created", <%= file_name %>_id: <%= file_name %>.id)
36
+ <%= file_name %>
37
+ end
38
+ end
39
+
40
+ # Update an existing <%= file_name %>
41
+ #
42
+ # @param <%= file_name %>_id [Integer, String] <%= class_name %> ID
43
+ # @param attributes [Hash] Attributes to update
44
+ # @return [<%= class_name %>] Updated <%= file_name %>
45
+ # @raise [ActiveRecord::RecordNotFound] If <%= file_name %> not found
46
+ # @raise [ActiveDomain::Domain::UnauthorizedError] If not authorized
47
+ def update_<%= file_name %>(<%= file_name %>_id, attributes)
48
+ <%= file_name %> = <%= class_name %>.find(<%= file_name %>_id)
49
+
50
+ # Authorization check
51
+ policy = <%= class_name %>Policy.new(current_user, <%= file_name %>)
52
+ authorize!(policy, :update?)
53
+
54
+ <%= class_name %>.transaction do
55
+ <%= file_name %>.update!(attributes)
56
+
57
+ # Event published via ActiveRecord integration
58
+ log(:info, "<%= class_name %> updated", <%= file_name %>_id: <%= file_name %>.id, changes: <%= file_name %>.saved_changes.keys)
59
+ <%= file_name %>
60
+ end
61
+ end
62
+
63
+ # Delete a <%= file_name %>
64
+ #
65
+ # @param <%= file_name %>_id [Integer, String] <%= class_name %> ID
66
+ # @return [Boolean] True if deleted
67
+ # @raise [ActiveRecord::RecordNotFound] If <%= file_name %> not found
68
+ # @raise [ActiveDomain::Domain::UnauthorizedError] If not authorized
69
+ def delete_<%= file_name %>(<%= file_name %>_id)
70
+ <%= file_name %> = <%= class_name %>.find(<%= file_name %>_id)
71
+
72
+ # Authorization check
73
+ policy = <%= class_name %>Policy.new(current_user, <%= file_name %>)
74
+ authorize!(policy, :destroy?)
75
+
76
+ <%= class_name %>.transaction do
77
+ <%= file_name %>.destroy!
78
+
79
+ # Event published via ActiveRecord integration
80
+ log(:info, "<%= class_name %> deleted", <%= file_name %>_id: <%= file_name %>_id)
81
+ true
82
+ end
83
+ end
84
+
85
+ # List <%= plural_name %> with policy scope
86
+ #
87
+ # @param scope [ActiveRecord::Relation] Optional base scope
88
+ # @return [ActiveRecord::Relation] Scoped <%= plural_name %>
89
+ def list_<%= plural_name %>(scope = <%= class_name %>.all)
90
+ policy_scope(scope, <%= class_name %>Policy)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= domain_module_name %>
4
+ # Setup event handlers for <%= file_name %> domain
5
+ #
6
+ # This file is automatically loaded by ActiveDomain::Railtie
7
+ # when the Rails application starts.
8
+ def self.setup!
9
+ # Register standard handlers (audit and metrics)
10
+ # This one line replaces ~50 lines of boilerplate!
11
+ ActiveDomain::Event::Registration.register_standard_handlers(
12
+ domain: '<%= file_name %>',
13
+ events: %w[created updated deleted],
14
+ include_audit: true,
15
+ include_metrics: true
16
+ )
17
+
18
+ # Register custom handlers here
19
+ # Example:
20
+ #
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)
24
+
25
+ Rails.logger.info "[<%= domain_module_name %>] Domain setup complete"
26
+ end
27
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module SmartDomain
6
+ module Generators
7
+ # Generator for installing SmartDomain in a Rails application
8
+ #
9
+ # Usage:
10
+ # rails generate smart_domain:install
11
+ #
12
+ # Creates:
13
+ # - config/initializers/smart_domain.rb
14
+ # - app/domains/ directory
15
+ # - app/events/ directory
16
+ # - app/handlers/ directory
17
+ # - app/policies/ directory
18
+ class InstallGenerator < Rails::Generators::Base
19
+ source_root File.expand_path("templates", __dir__)
20
+
21
+ desc "Install SmartDomain into your Rails application"
22
+
23
+ # Create initializer
24
+ def create_initializer
25
+ template "initializer.rb", "config/initializers/smart_domain.rb"
26
+ end
27
+
28
+ # Create directory structure
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"
35
+ end
36
+
37
+ # Create base event class
38
+ def create_application_event
39
+ template "application_event.rb", "app/events/application_event.rb"
40
+ end
41
+
42
+ # Create base policy class
43
+ def create_application_policy
44
+ template "application_policy.rb", "app/policies/application_policy.rb"
45
+ end
46
+
47
+ # Create base service class
48
+ def create_application_service
49
+ template "application_service.rb", "app/services/application_service.rb"
50
+ end
51
+
52
+ # Show post-install message
53
+ def show_readme
54
+ readme "README" if behavior == :invoke
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,28 @@
1
+ ===============================================================================
2
+
3
+ ActiveDomain has been installed!
4
+
5
+ Created files:
6
+ config/initializers/active_domain.rb - Configuration
7
+ app/events/application_event.rb - Base event class
8
+ app/policies/application_policy.rb - Base policy class
9
+ app/services/application_service.rb - Base service class
10
+
11
+ Created directories:
12
+ app/domains/ - Domain modules go here
13
+ app/events/ - Domain events go here
14
+ app/handlers/ - Custom event handlers go here
15
+ app/policies/ - Domain policies go here
16
+ app/services/ - Domain services go here
17
+
18
+ Next steps:
19
+
20
+ 1. Review and configure config/initializers/active_domain.rb
21
+
22
+ 2. Generate your first domain:
23
+ rails generate active_domain:domain User
24
+
25
+ 3. Read the documentation:
26
+ https://github.com/rachidalmaach/active_domain
27
+
28
+ ===============================================================================
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for all domain events in this application
4
+ #
5
+ # Inherit from this class to create domain-specific events
6
+ #
7
+ # Example:
8
+ # class UserCreatedEvent < ApplicationEvent
9
+ # attribute :user_id, :string
10
+ # attribute :email, :string
11
+ # validates :user_id, :email, presence: true
12
+ # end
13
+ class ApplicationEvent < SmartDomain::Event::Base
14
+ # Add application-wide event fields here
15
+ # Example:
16
+ # attribute :tenant_id, :string
17
+ # validates :tenant_id, presence: true
18
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for all domain policies in this application
4
+ #
5
+ # Inherit from this class to create domain-specific policies
6
+ #
7
+ # Example:
8
+ # class UserPolicy < ApplicationPolicy
9
+ # def update?
10
+ # user.admin? || owner?
11
+ # end
12
+ #
13
+ # def destroy?
14
+ # user.admin?
15
+ # end
16
+ # end
17
+ class ApplicationPolicy < SmartDomain::Domain::Policy
18
+ # Add application-wide policy methods here
19
+ # Example:
20
+ # def manager?
21
+ # user.present? && user.role == 'manager'
22
+ # end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base class for all domain services in this application
4
+ #
5
+ # Inherit from this class to create domain-specific services
6
+ #
7
+ # Example:
8
+ # class UserService < ApplicationService
9
+ # def create_user(attributes)
10
+ # User.transaction do
11
+ # user = User.create!(attributes)
12
+ # event = build_event(UserCreatedEvent, ...)
13
+ # publish_after_commit(event)
14
+ # user
15
+ # end
16
+ # end
17
+ # end
18
+ class ApplicationService < SmartDomain::Domain::Service
19
+ # Add application-wide service methods here
20
+ # Example:
21
+ # def current_organization
22
+ # @current_organization ||= Organization.find(current_organization_id)
23
+ # end
24
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SmartDomain configuration
4
+ SmartDomain.configure do |config|
5
+ # Event bus adapter (:memory, :redis, :active_job)
6
+ # :memory is synchronous and suitable for development/testing
7
+ # :redis and :active_job are asynchronous and suitable for production
8
+ config.event_bus_adapter = :memory
9
+
10
+ # Enable automatic writes to audit_events table
11
+ # Set to true if you have an AuditEvent model for compliance
12
+ config.audit_table_enabled = false
13
+
14
+ # Enable multi-tenancy support
15
+ # Set to true if your application is multi-tenant
16
+ config.multi_tenancy_enabled = false
17
+
18
+ # Key used for tenant identification (e.g., :organization_id, :account_id)
19
+ config.tenant_key = :organization_id
20
+
21
+ # Use ActiveJob for asynchronous event handling
22
+ # Requires config.event_bus_adapter to support async (not :memory)
23
+ config.async_handlers = false
24
+
25
+ # Logger instance
26
+ config.logger = Rails.logger
27
+ end
28
+
29
+ # Example: Register standard handlers for a domain
30
+ # Uncomment and modify for your domains:
31
+ #
32
+ # SmartDomain::Event::Registration.register_standard_handlers(
33
+ # domain: 'user',
34
+ # events: %w[created updated deleted],
35
+ # include_audit: true,
36
+ # include_metrics: true
37
+ # )
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module SmartDomain
6
+ # Configuration for SmartDomain gem.
7
+ #
8
+ # Configure the gem in an initializer:
9
+ #
10
+ # @example config/initializers/smart_domain.rb
11
+ # SmartDomain.configure do |config|
12
+ # config.event_bus_adapter = :memory
13
+ # config.audit_table_enabled = true
14
+ # config.multi_tenancy_enabled = true
15
+ # config.tenant_key = :organization_id
16
+ # config.async_handlers = false
17
+ # config.logger = Rails.logger
18
+ # end
19
+ class Configuration
20
+ # Event bus adapter to use (:memory, :redis, :active_job)
21
+ # @return [Symbol, Object] Adapter symbol or adapter instance
22
+ attr_accessor :event_bus_adapter
23
+
24
+ # Enable automatic writes to audit_events table
25
+ # @return [Boolean]
26
+ attr_accessor :audit_table_enabled
27
+
28
+ # Enable multi-tenancy support
29
+ # @return [Boolean]
30
+ attr_accessor :multi_tenancy_enabled
31
+
32
+ # Key used for tenant identification (e.g., :organization_id, :account_id)
33
+ # @return [Symbol]
34
+ attr_accessor :tenant_key
35
+
36
+ # Use ActiveJob for asynchronous event handling
37
+ # @return [Boolean]
38
+ attr_accessor :async_handlers
39
+
40
+ # Logger instance for SmartDomain
41
+ # @return [Logger]
42
+ attr_accessor :logger
43
+
44
+ # Initialize configuration with defaults
45
+ def initialize
46
+ @event_bus_adapter = nil # Will use Memory adapter
47
+ @audit_table_enabled = false
48
+ @multi_tenancy_enabled = false
49
+ @tenant_key = :organization_id
50
+ @async_handlers = false
51
+ @logger = Logger.new($stdout)
52
+ end
53
+
54
+ # Check if audit table writes are enabled
55
+ # @return [Boolean]
56
+ def audit_table_enabled?
57
+ @audit_table_enabled == true
58
+ end
59
+
60
+ # Check if multi-tenancy is enabled
61
+ # @return [Boolean]
62
+ def multi_tenancy_enabled?
63
+ @multi_tenancy_enabled == true
64
+ end
65
+
66
+ # Check if async handlers are enabled
67
+ # @return [Boolean]
68
+ def async_handlers?
69
+ @async_handlers == true
70
+ end
71
+ end
72
+
73
+ # Get or create the configuration instance
74
+ # @return [SmartDomain::Configuration]
75
+ def self.configuration
76
+ @configuration ||= Configuration.new
77
+ end
78
+
79
+ # Configure SmartDomain
80
+ #
81
+ # @example
82
+ # SmartDomain.configure do |config|
83
+ # config.event_bus_adapter = :memory
84
+ # config.audit_table_enabled = true
85
+ # end
86
+ #
87
+ # @yield [Configuration] The configuration object
88
+ def self.configure
89
+ yield(configuration)
90
+ end
91
+
92
+ # Reset configuration to defaults (useful for testing)
93
+ # @api private
94
+ def self.reset_configuration!
95
+ @configuration = Configuration.new
96
+ end
97
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartDomain
4
+ module Domain
5
+ # Base exception for all domain errors
6
+ #
7
+ # Domain exceptions represent business rule violations and should be
8
+ # handled differently from technical errors (like database errors).
9
+ #
10
+ # @example Define domain-specific exceptions
11
+ # class UserNotFoundError < SmartDomain::Domain::Error
12
+ # def initialize(user_id)
13
+ # super("User not found: #{user_id}")
14
+ # end
15
+ # end
16
+ #
17
+ # class UserAlreadyExistsError < SmartDomain::Domain::Error
18
+ # def initialize(email)
19
+ # super("User with email #{email} already exists")
20
+ # end
21
+ # end
22
+ class Error < StandardError
23
+ attr_reader :code, :details
24
+
25
+ # Initialize domain error
26
+ #
27
+ # @param message [String] Error message
28
+ # @param code [Symbol, nil] Error code for programmatic handling
29
+ # @param details [Hash, nil] Additional error details
30
+ def initialize(message, code: nil, details: {})
31
+ super(message)
32
+ @code = code
33
+ @details = details
34
+ end
35
+
36
+ # Convert error to hash (useful for API responses)
37
+ #
38
+ # @return [Hash] Error representation
39
+ def to_h
40
+ {
41
+ error: self.class.name,
42
+ message: message,
43
+ code: code,
44
+ details: details
45
+ }.compact
46
+ end
47
+
48
+ alias to_hash to_h
49
+ end
50
+
51
+ # Error raised when an entity is not found
52
+ #
53
+ # @example
54
+ # raise SmartDomain::Domain::NotFoundError.new('User', user_id)
55
+ class NotFoundError < Error
56
+ def initialize(entity_type, entity_id)
57
+ super(
58
+ "#{entity_type} not found: #{entity_id}",
59
+ code: :not_found,
60
+ details: { entity_type: entity_type, entity_id: entity_id }
61
+ )
62
+ end
63
+ end
64
+
65
+ # Error raised when an entity already exists
66
+ #
67
+ # @example
68
+ # raise SmartDomain::Domain::AlreadyExistsError.new('User', 'email', email)
69
+ class AlreadyExistsError < Error
70
+ def initialize(entity_type, attribute, value)
71
+ super(
72
+ "#{entity_type} with #{attribute} '#{value}' already exists",
73
+ code: :already_exists,
74
+ details: { entity_type: entity_type, attribute: attribute, value: value }
75
+ )
76
+ end
77
+ end
78
+
79
+ # Error raised when a business rule is violated
80
+ #
81
+ # @example
82
+ # raise SmartDomain::Domain::BusinessRuleError.new(
83
+ # 'Cannot delete user with active orders'
84
+ # )
85
+ class BusinessRuleError < Error
86
+ def initialize(message, code: :business_rule_violation, details: {})
87
+ super(message, code: code, details: details)
88
+ end
89
+ end
90
+
91
+ # Error raised when an invalid state transition is attempted
92
+ #
93
+ # @example
94
+ # raise SmartDomain::Domain::InvalidStateError.new(
95
+ # 'User',
96
+ # from: 'suspended',
97
+ # to: 'active',
98
+ # reason: 'User must be pending to activate'
99
+ # )
100
+ class InvalidStateError < Error
101
+ def initialize(entity_type, from:, to:, reason: nil)
102
+ message = "Invalid state transition for #{entity_type}: #{from} -> #{to}"
103
+ message += " (#{reason})" if reason
104
+
105
+ super(
106
+ message,
107
+ code: :invalid_state,
108
+ details: { entity_type: entity_type, from: from, to: to, reason: reason }
109
+ )
110
+ end
111
+ end
112
+
113
+ # Error raised when validation fails
114
+ #
115
+ # @example
116
+ # raise SmartDomain::Domain::ValidationError.new(
117
+ # 'User validation failed',
118
+ # errors: { email: ['is required'], name: ['is too short'] }
119
+ # )
120
+ class ValidationError < Error
121
+ def initialize(message, errors: {})
122
+ super(
123
+ message,
124
+ code: :validation_failed,
125
+ details: { validation_errors: errors }
126
+ )
127
+ end
128
+ end
129
+
130
+ # Error raised when authorization fails
131
+ #
132
+ # @example
133
+ # raise SmartDomain::Domain::UnauthorizedError.new(
134
+ # 'User does not have permission to delete this resource'
135
+ # )
136
+ class UnauthorizedError < Error
137
+ def initialize(message = "Unauthorized", action: nil, resource: nil)
138
+ super(
139
+ message,
140
+ code: :unauthorized,
141
+ details: { action: action, resource: resource }.compact
142
+ )
143
+ end
144
+ end
145
+
146
+ # Error raised when a required dependency is not available
147
+ #
148
+ # @example
149
+ # raise SmartDomain::Domain::DependencyError.new(
150
+ # 'Redis',
151
+ # 'Redis is required for caching'
152
+ # )
153
+ class DependencyError < Error
154
+ def initialize(dependency_name, message = nil)
155
+ message ||= "Required dependency not available: #{dependency_name}"
156
+ super(
157
+ message,
158
+ code: :dependency_missing,
159
+ details: { dependency: dependency_name }
160
+ )
161
+ end
162
+ end
163
+ end
164
+ end