smart_domain 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c99a8063c81eea7d1de36a368d27803fd13807ae99271c4d184d1f58c82d67d
4
- data.tar.gz: 41c04fc2729bfc7861dbbf396e65c4d04a843c353f493796e72b89af10b1f388
3
+ metadata.gz: dc4986884824f0ebda3f3ea7051e7303bb416b7497f1b3213a749bdbd2a80918
4
+ data.tar.gz: 5a65d52f74b31c7ebc0da5d8a63d8b8942b5262746de39826f185d61129f32e7
5
5
  SHA512:
6
- metadata.gz: 512d73e5e5081d01a2aa204f2d02d27e217d87f143863a5ab890a738ccdd637706e6b21927f08f67c0bfd5bdf55990d3ac8a042a68b5442869d99da844dea9f5
7
- data.tar.gz: d3b7c9bae2e7787d7b2bda3604cca244069879584d2d18ce0ef0f60437e4ad7d14d6e6bbbad919059ee3c215bb021d374f31719718ed6b8ae48aa57990f788cc
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/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.
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
@@ -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
 
@@ -37,7 +37,7 @@ module SmartDomain
37
37
  # @param event [SmartDomain::Event::Base] Event to handle
38
38
  # @raise [NotImplementedError] If not implemented by subclass
39
39
  def handle(event)
40
- raise NotImplementedError, "#{self.class.name} must implement #handle(event)"
40
+ raise NotImplementedError, 'Subclasses must implement #handle(event)'
41
41
  end
42
42
 
43
43
  # Check if this handler can handle a specific event type
@@ -48,7 +48,7 @@ module SmartDomain
48
48
  # @param event_type [String] Event type to check
49
49
  # @return [Boolean] True if handler can handle this event type
50
50
  def can_handle?(event_type)
51
- raise NotImplementedError, "#{self.class.name} must implement #can_handle?(event_type)"
51
+ raise NotImplementedError, 'Subclasses must implement #can_handle?(event_type)'
52
52
  end
53
53
 
54
54
  # Handle event asynchronously using ActiveJob
@@ -58,12 +58,14 @@ module SmartDomain
58
58
  #
59
59
  # @param event [SmartDomain::Event::Base] Event to handle
60
60
  # @raise [RuntimeError] If ActiveJob is not loaded
61
+ # @raise [ValidationError] If event is invalid
61
62
  def handle_async(event)
62
63
  unless defined?(ActiveJob)
63
64
  raise "ActiveJob is required for async event handling. Please require 'active_job' in your application."
64
65
  end
65
66
 
66
- event.validate!
67
+ raise ValidationError, "Event validation failed: #{event.errors.full_messages.join(', ')}" unless event.valid?
68
+
67
69
  HandlerJob.perform_later(self.class.name, event.to_h)
68
70
  end
69
71
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/concern"
3
+ require 'active_support/concern'
4
4
 
5
5
  module SmartDomain
6
6
  module Event
@@ -89,6 +89,14 @@ module SmartDomain
89
89
  attribute :changed_fields, default: -> { [] }
90
90
  attribute :old_values, default: -> { {} }
91
91
  attribute :new_values, default: -> { {} }
92
+
93
+ validate :changed_fields_must_not_be_empty
94
+
95
+ private
96
+
97
+ def changed_fields_must_not_be_empty
98
+ errors.add(:changed_fields, "can't be blank") if changed_fields.blank? || changed_fields.empty?
99
+ end
92
100
  end
93
101
 
94
102
  # Helper to extract changes from an ActiveRecord model
@@ -150,6 +158,8 @@ module SmartDomain
150
158
 
151
159
  included do
152
160
  attribute :reason, :string
161
+
162
+ validates :reason, presence: true
153
163
  end
154
164
  end
155
165
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../handlers/audit_handler"
4
- require_relative "../handlers/metrics_handler"
3
+ require_relative '../handlers/audit_handler'
4
+ require_relative '../handlers/metrics_handler'
5
5
 
6
6
  module SmartDomain
7
7
  module Event
@@ -86,8 +86,8 @@ module SmartDomain
86
86
  # Log what was registered
87
87
  if include_audit || include_metrics
88
88
  handlers_registered = []
89
- handlers_registered << "audit" if include_audit
90
- handlers_registered << "metrics" if include_metrics
89
+ handlers_registered << 'audit' if include_audit
90
+ handlers_registered << 'metrics' if include_metrics
91
91
 
92
92
  logger.info "[SmartDomain::Registration] Standard handlers registered for #{domain} domain: " \
93
93
  "#{handlers_registered.join(', ')} (#{events.size} events)"
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Load the domain generator
4
- require_relative "../../generators/smart_domain/domain/domain_generator"
4
+ require_relative '../../generators/smart_domain/domain/domain_generator'
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Load the install generator
4
- require_relative "../../generators/smart_domain/install/install_generator"
4
+ require_relative '../../generators/smart_domain/install/install_generator'
@@ -41,7 +41,7 @@ module SmartDomain
41
41
  # @param event_type [String] Event type to check
42
42
  # @return [Boolean] True if event belongs to this domain
43
43
  def can_handle?(event_type)
44
- return true if @domain == "*"
44
+ return true if @domain == '*'
45
45
 
46
46
  event_type.start_with?("#{@domain}.")
47
47
  end
@@ -49,7 +49,7 @@ module SmartDomain
49
49
  # Handle audit logging for an event
50
50
  # @param event [SmartDomain::Event::Base] Event to audit
51
51
  def handle(event)
52
- action = event.event_type.split(".").last
52
+ action = event.event_type.split('.').last
53
53
 
54
54
  # 1. Log to Rails logger (structured)
55
55
  log_audit_event(event, action)
@@ -117,17 +117,15 @@ module SmartDomain
117
117
  return unless defined?(AuditEvent)
118
118
 
119
119
  AuditEvent.create!(
120
- event_type: event.event_type.upcase.tr(".", "_"),
121
- event_category: map_event_category(event.event_type),
122
- user_id: extract_actor_id(event),
120
+ event_id: event.event_id,
121
+ event_type: event.event_type,
122
+ aggregate_id: event.aggregate_id,
123
+ aggregate_type: event.aggregate_type,
123
124
  organization_id: event.organization_id,
124
- ip_address: extract_ip_address(event),
125
- user_agent: extract_user_agent(event),
126
- old_values: extract_old_values(event),
127
- new_values: extract_new_values(event),
128
- occurred_at: event.occurred_at,
125
+ category: map_event_category(event.event_type),
129
126
  risk_level: assess_risk_level(event.event_type),
130
- compliance_flags: build_compliance_flags(event)
127
+ event_data: event.to_h,
128
+ occurred_at: event.occurred_at
131
129
  )
132
130
  rescue StandardError => e
133
131
  @logger.warn "[SmartDomain::AuditHandler] Failed to write to audit table: #{e.message}"
@@ -138,14 +136,14 @@ module SmartDomain
138
136
  # @return [String] Audit category
139
137
  def map_event_category(event_type)
140
138
  case event_type
141
- when /^auth\./
142
- "authentication"
139
+ when /^auth\.|logged_in|logged_out|login|logout|authenticated|password/
140
+ 'authentication'
143
141
  when /accessed|viewed|patient\./
144
- "data_access"
142
+ 'data_access'
145
143
  when /created|updated|deleted|assigned|removed/
146
- "admin_action"
144
+ 'admin_action'
147
145
  else
148
- "system_event"
146
+ 'system_event'
149
147
  end
150
148
  end
151
149
 
@@ -155,11 +153,11 @@ module SmartDomain
155
153
  def assess_risk_level(event_type)
156
154
  case event_type
157
155
  when /suspended|deleted|revoked|failed|rejected/
158
- "HIGH"
156
+ 'HIGH'
159
157
  when /updated|changed|assigned/
160
- "MEDIUM"
158
+ 'MEDIUM'
161
159
  else
162
- "LOW"
160
+ 'LOW'
163
161
  end
164
162
  end
165
163
 
@@ -167,35 +165,35 @@ module SmartDomain
167
165
  # @param event [SmartDomain::Event::Base] Event
168
166
  # @return [String, nil] Actor ID
169
167
  def extract_actor_id(event)
170
- event.try(:actor_id) || event.attributes["actor_id"]
168
+ event.try(:actor_id) || event.attributes['actor_id']
171
169
  end
172
170
 
173
171
  # Extract ip_address from event (SecurityContextMixin)
174
172
  # @param event [SmartDomain::Event::Base] Event
175
173
  # @return [String, nil] IP address
176
174
  def extract_ip_address(event)
177
- event.try(:ip_address) || event.attributes["ip_address"]
175
+ event.try(:ip_address) || event.attributes['ip_address']
178
176
  end
179
177
 
180
178
  # Extract user_agent from event (SecurityContextMixin)
181
179
  # @param event [SmartDomain::Event::Base] Event
182
180
  # @return [String, nil] User agent
183
181
  def extract_user_agent(event)
184
- event.try(:user_agent) || event.attributes["user_agent"]
182
+ event.try(:user_agent) || event.attributes['user_agent']
185
183
  end
186
184
 
187
185
  # Extract old_values from event (ChangeTrackingMixin)
188
186
  # @param event [SmartDomain::Event::Base] Event
189
187
  # @return [Hash, nil] Old values
190
188
  def extract_old_values(event)
191
- event.try(:old_values) || event.attributes["old_values"]
189
+ event.try(:old_values) || event.attributes['old_values']
192
190
  end
193
191
 
194
192
  # Extract new_values from event (ChangeTrackingMixin)
195
193
  # @param event [SmartDomain::Event::Base] Event
196
194
  # @return [Hash, nil] New values
197
195
  def extract_new_values(event)
198
- event.try(:new_values) || event.attributes["new_values"]
196
+ event.try(:new_values) || event.attributes['new_values']
199
197
  end
200
198
 
201
199
  # Build compliance flags for audit record
@@ -36,7 +36,7 @@ module SmartDomain
36
36
  # @param event_type [String] Event type to check
37
37
  # @return [Boolean] True if event belongs to this domain
38
38
  def can_handle?(event_type)
39
- return true if @domain == "*"
39
+ return true if @domain == '*'
40
40
 
41
41
  event_type.start_with?("#{@domain}.")
42
42
  end
@@ -47,7 +47,14 @@ module SmartDomain
47
47
  metric_name = build_metric_name(event)
48
48
  tags = build_metric_tags(event)
49
49
 
50
+ # Emit counter metric
50
51
  emit_metric(metric_name, tags)
52
+
53
+ # Emit timing metric if duration is present
54
+ if event.respond_to?(:duration) && event.duration
55
+ timing_name = "#{metric_name}.duration"
56
+ emit_metric(timing_name, tags.merge(duration_ms: event.duration))
57
+ end
51
58
  rescue StandardError => e
52
59
  # Never fail on metrics handler errors
53
60
  @logger.warn "[SmartDomain::MetricsHandler] Metrics collection failed: #{e.message}"
@@ -145,14 +145,12 @@ module SmartDomain
145
145
  return if @pending_domain_events.blank?
146
146
 
147
147
  @pending_domain_events.each do |event|
148
- begin
149
- SmartDomain::Event.bus.publish(event)
150
- rescue StandardError => e
151
- # Log error but don't raise (events should be fire-and-forget)
152
- logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
153
- logger.error "[SmartDomain] Failed to publish event: #{e.message}"
154
- logger.error e.backtrace.join("\n")
155
- end
148
+ SmartDomain::Event.bus.publish(event)
149
+ rescue StandardError => e
150
+ # Log error but don't raise (events should be fire-and-forget)
151
+ logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
152
+ logger.error "[SmartDomain] Failed to publish event: #{e.message}"
153
+ logger.error e.backtrace.join("\n")
156
154
  end
157
155
 
158
156
  clear_domain_events
@@ -12,8 +12,8 @@ module SmartDomain
12
12
 
13
13
  # Add generators to load path
14
14
  generators do
15
- require_relative "generators/install_generator"
16
- require_relative "generators/domain_generator"
15
+ require_relative 'generators/install_generator'
16
+ require_relative 'generators/domain_generator'
17
17
  end
18
18
 
19
19
  # Configuration hook
@@ -27,35 +27,35 @@ module SmartDomain
27
27
 
28
28
  # Rake tasks
29
29
  rake_tasks do
30
- load "smart_domain/tasks/domains.rake"
30
+ load 'smart_domain/tasks/domains.rake'
31
31
  end
32
32
 
33
- private
34
-
35
33
  # Load all domain setup files
36
34
  def self.load_domain_setups
37
- setup_files = Dir[Rails.root.join("app/domains/**/setup.rb")]
35
+ setup_files = Dir[Rails.root.join('app/domains/**/setup.rb')]
38
36
 
39
37
  setup_files.each do |setup_file|
40
- begin
41
- require setup_file
42
-
43
- # Extract domain module name from path
44
- # e.g., app/domains/user_management/setup.rb -> UserManagement
45
- domain_path = setup_file.gsub(Rails.root.join("app/domains/").to_s, "")
46
- domain_name = domain_path.split("/").first.camelize
47
-
48
- # Call setup! method if defined
49
- domain_module = domain_name.constantize rescue nil
50
- if domain_module && domain_module.respond_to?(:setup!)
51
- domain_module.setup!
52
- Rails.logger.info "[SmartDomain] Loaded domain: #{domain_name}"
53
- end
54
- rescue StandardError => e
55
- Rails.logger.error "[SmartDomain] Failed to load domain setup: #{setup_file}"
56
- Rails.logger.error e.message
57
- Rails.logger.error e.backtrace.join("\n")
38
+ require setup_file
39
+
40
+ # Extract domain module name from path
41
+ # e.g., app/domains/user_management/setup.rb -> UserManagement
42
+ domain_path = setup_file.gsub(Rails.root.join('app/domains/').to_s, '')
43
+ domain_name = domain_path.split('/').first.camelize
44
+
45
+ # Call setup! method if defined
46
+ domain_module = begin
47
+ domain_name.constantize
48
+ rescue StandardError
49
+ nil
50
+ end
51
+ if domain_module.respond_to?(:setup!)
52
+ domain_module.setup!
53
+ Rails.logger.info "[SmartDomain] Loaded domain: #{domain_name}"
58
54
  end
55
+ rescue StandardError => e
56
+ Rails.logger.error "[SmartDomain] Failed to load domain setup: #{setup_file}"
57
+ Rails.logger.error e.message
58
+ Rails.logger.error e.backtrace.join("\n")
59
59
  end
60
60
  end
61
61
  end
@@ -1,43 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  namespace :active_domain do
4
- desc "List all registered domains"
4
+ desc 'List all registered domains'
5
5
  task domains: :environment do
6
- setup_files = Dir[Rails.root.join("app/domains/**/setup.rb")]
6
+ setup_files = Dir[Rails.root.join('app/domains/**/setup.rb')]
7
7
 
8
8
  if setup_files.empty?
9
- puts "No domains found in app/domains/"
10
- puts "Generate your first domain with: rails generate active_domain:domain User"
9
+ puts 'No domains found in app/domains/'
10
+ puts 'Generate your first domain with: rails generate active_domain:domain User'
11
11
  else
12
- puts "Registered domains:"
12
+ puts 'Registered domains:'
13
13
  setup_files.each do |setup_file|
14
- domain_path = setup_file.gsub(Rails.root.join("app/domains/").to_s, "")
15
- domain_name = domain_path.split("/").first
14
+ domain_path = setup_file.gsub(Rails.root.join('app/domains/').to_s, '')
15
+ domain_name = domain_path.split('/').first
16
16
  puts " - #{domain_name}"
17
17
  end
18
18
  puts "\nTotal: #{setup_files.size} domain(s)"
19
19
  end
20
20
  end
21
21
 
22
- desc "Reload all domain setups"
22
+ desc 'Reload all domain setups'
23
23
  task reload: :environment do
24
- setup_files = Dir[Rails.root.join("app/domains/**/setup.rb")]
24
+ setup_files = Dir[Rails.root.join('app/domains/**/setup.rb')]
25
25
 
26
26
  puts "Reloading #{setup_files.size} domain(s)..."
27
27
 
28
28
  setup_files.each do |setup_file|
29
29
  load setup_file
30
30
 
31
- domain_path = setup_file.gsub(Rails.root.join("app/domains/").to_s, "")
32
- domain_name = domain_path.split("/").first.camelize
31
+ domain_path = setup_file.gsub(Rails.root.join('app/domains/').to_s, '')
32
+ domain_name = domain_path.split('/').first.camelize
33
33
 
34
- domain_module = domain_name.constantize rescue nil
35
- if domain_module && domain_module.respond_to?(:setup!)
34
+ domain_module = begin
35
+ domain_name.constantize
36
+ rescue StandardError
37
+ nil
38
+ end
39
+ if domain_module.respond_to?(:setup!)
36
40
  domain_module.setup!
37
41
  puts " ✓ #{domain_name}"
38
42
  end
39
43
  end
40
44
 
41
- puts "Done!"
45
+ puts 'Done!'
42
46
  end
43
47
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SmartDomain
4
- VERSION = "0.1.1"
4
+ VERSION = '0.1.2'
5
5
  end
data/lib/smart_domain.rb CHANGED
@@ -1,38 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_model"
4
- require "active_support"
5
- require "active_support/core_ext"
6
- require "active_record"
7
- require "logger"
3
+ require 'active_model'
4
+ require 'active_support'
5
+ require 'active_support/core_ext'
6
+ require 'active_record'
7
+ require 'logger'
8
8
 
9
- require_relative "smart_domain/version"
10
- require_relative "smart_domain/configuration"
9
+ require_relative 'smart_domain/version'
10
+ require_relative 'smart_domain/configuration'
11
11
 
12
12
  # Event system
13
- require_relative "smart_domain/event/base"
14
- require_relative "smart_domain/event/mixins"
15
- require_relative "smart_domain/event/handler"
16
- require_relative "smart_domain/event/adapters/memory"
13
+ require_relative 'smart_domain/event/base'
14
+ require_relative 'smart_domain/event/mixins'
15
+ require_relative 'smart_domain/event/handler'
16
+ require_relative 'smart_domain/event/adapters/memory'
17
17
 
18
18
  # Handlers
19
- require_relative "smart_domain/handlers/audit_handler"
20
- require_relative "smart_domain/handlers/metrics_handler"
19
+ require_relative 'smart_domain/handlers/audit_handler'
20
+ require_relative 'smart_domain/handlers/metrics_handler'
21
21
 
22
22
  # Event registration
23
- require_relative "smart_domain/event/registration"
23
+ require_relative 'smart_domain/event/registration'
24
24
 
25
25
  # Domain patterns
26
- require_relative "smart_domain/domain/exceptions"
27
- require_relative "smart_domain/domain/policy"
28
- require_relative "smart_domain/domain/service"
26
+ require_relative 'smart_domain/domain/exceptions'
27
+ require_relative 'smart_domain/domain/policy'
28
+ require_relative 'smart_domain/domain/service'
29
29
 
30
30
  # Rails integration
31
- require_relative "smart_domain/integration/active_record"
32
- require_relative "smart_domain/integration/multi_tenancy"
31
+ require_relative 'smart_domain/integration/active_record'
32
+ require_relative 'smart_domain/integration/multi_tenancy'
33
33
 
34
34
  # Railtie (loads automatically if Rails is present)
35
- require_relative "smart_domain/railtie" if defined?(Rails::Railtie)
35
+ require_relative 'smart_domain/railtie' if defined?(Rails::Railtie)
36
36
 
37
37
  # SmartDomain - Domain-Driven Design and Event-Driven Architecture for Rails
38
38
  #
data/smart_domain.gemspec CHANGED
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/smart_domain/version"
3
+ require_relative 'lib/smart_domain/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = "smart_domain"
6
+ spec.name = 'smart_domain'
7
7
  spec.version = SmartDomain::VERSION
8
- spec.authors = ["Rachid Al Maach"]
9
- spec.email = ["rachid@qraft.nl"]
8
+ spec.authors = ['Rachid Al Maach']
9
+ spec.email = ['rachid@qraft.nl']
10
10
 
11
- spec.summary = "Smart Domain-Driven Design and Event-Driven Architecture for Rails"
11
+ spec.summary = 'Smart Domain-Driven Design and Event-Driven Architecture for Rails'
12
12
  spec.description = <<~DESC
13
13
  SmartDomain brings battle-tested DDD/EDA patterns to Rails applications.
14
14
  Inspired by the Aeyes healthcare platform, it provides domain events,
@@ -16,13 +16,14 @@ Gem::Specification.new do |spec|
16
16
  Features 70% boilerplate reduction through intelligent event handling and
17
17
  AI-augmented development patterns.
18
18
  DESC
19
- spec.homepage = "https://github.com/rachid/smart_domain"
20
- spec.license = "MIT"
21
- spec.required_ruby_version = ">= 3.0.0"
19
+ spec.homepage = 'https://github.com/rachid/smart_domain'
20
+ spec.license = 'MIT'
21
+ spec.required_ruby_version = '>= 3.0.0'
22
22
 
23
- spec.metadata["homepage_uri"] = spec.homepage
24
- spec.metadata["source_code_uri"] = "https://github.com/rachid/smart_domain"
25
- spec.metadata["changelog_uri"] = "https://github.com/rachid/smart_domain/blob/main/CHANGELOG.md"
23
+ spec.metadata['homepage_uri'] = spec.homepage
24
+ spec.metadata['source_code_uri'] = 'https://github.com/rachid/smart_domain'
25
+ spec.metadata['changelog_uri'] = 'https://github.com/rachid/smart_domain/blob/main/CHANGELOG.md'
26
+ spec.metadata['rubygems_mfa_required'] = 'true'
26
27
 
27
28
  spec.files = Dir.chdir(__dir__) do
28
29
  `git ls-files -z`.split("\x0").reject do |f|
@@ -31,23 +32,23 @@ Gem::Specification.new do |spec|
31
32
  end
32
33
  end
33
34
 
34
- spec.bindir = "exe"
35
+ spec.bindir = 'exe'
35
36
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
36
- spec.require_paths = ["lib"]
37
+ spec.require_paths = ['lib']
37
38
 
38
39
  # Core dependencies
39
- spec.add_dependency "rails", ">= 7.0"
40
- spec.add_dependency "activemodel", ">= 7.0"
41
- spec.add_dependency "activesupport", ">= 7.0"
42
- spec.add_dependency "activerecord", ">= 7.0"
40
+ spec.add_dependency 'activemodel', '>= 7.0'
41
+ spec.add_dependency 'activerecord', '>= 7.0'
42
+ spec.add_dependency 'activesupport', '>= 7.0'
43
+ spec.add_dependency 'rails', '>= 7.0'
43
44
 
44
45
  # Development dependencies
45
- spec.add_development_dependency "rspec", "~> 3.12"
46
- spec.add_development_dependency "rspec-rails", "~> 6.0"
47
- spec.add_development_dependency "factory_bot_rails", "~> 6.2"
48
- spec.add_development_dependency "faker", "~> 3.2"
49
- spec.add_development_dependency "rubocop", "~> 1.50"
50
- spec.add_development_dependency "rubocop-rails", "~> 2.19"
51
- spec.add_development_dependency "rubocop-rspec", "~> 2.20"
52
- spec.add_development_dependency "sqlite3", ">= 2.1"
46
+ spec.add_development_dependency 'factory_bot_rails', '~> 6.2'
47
+ spec.add_development_dependency 'faker', '~> 3.2'
48
+ spec.add_development_dependency 'rspec', '~> 3.12'
49
+ spec.add_development_dependency 'rspec-rails', '~> 6.0'
50
+ spec.add_development_dependency 'rubocop', '~> 1.50'
51
+ spec.add_development_dependency 'rubocop-rails', '~> 2.19'
52
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.20'
53
+ spec.add_development_dependency 'sqlite3', '>= 2.1'
53
54
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_domain
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rachid Al Maach
@@ -11,7 +11,7 @@ cert_chain: []
11
11
  date: 2025-12-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
14
+ name: activemodel
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '7.0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: activemodel
28
+ name: activerecord
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '7.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: activerecord
56
+ name: rails
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,61 +67,61 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '7.0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rspec
70
+ name: factory_bot_rails
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '3.12'
75
+ version: '6.2'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '3.12'
82
+ version: '6.2'
83
83
  - !ruby/object:Gem::Dependency
84
- name: rspec-rails
84
+ name: faker
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '6.0'
89
+ version: '3.2'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '6.0'
96
+ version: '3.2'
97
97
  - !ruby/object:Gem::Dependency
98
- name: factory_bot_rails
98
+ name: rspec
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '6.2'
103
+ version: '3.12'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '6.2'
110
+ version: '3.12'
111
111
  - !ruby/object:Gem::Dependency
112
- name: faker
112
+ name: rspec-rails
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '3.2'
117
+ version: '6.0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '3.2'
124
+ version: '6.0'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: rubocop
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -191,6 +191,7 @@ extensions: []
191
191
  extra_rdoc_files: []
192
192
  files:
193
193
  - ".rspec"
194
+ - ".rubocop.yml"
194
195
  - CHANGELOG.md
195
196
  - LICENSE
196
197
  - README.md
@@ -286,7 +287,6 @@ files:
286
287
  - examples/blog_app/config/initializers/inflections.rb
287
288
  - examples/blog_app/config/initializers/smart_domain.rb
288
289
  - examples/blog_app/config/locales/en.yml
289
- - examples/blog_app/config/master.key
290
290
  - examples/blog_app/config/puma.rb
291
291
  - examples/blog_app/config/queue.yml
292
292
  - examples/blog_app/config/recurring.yml
@@ -349,7 +349,6 @@ files:
349
349
  - lib/smart_domain/railtie.rb
350
350
  - lib/smart_domain/tasks/domains.rake
351
351
  - lib/smart_domain/version.rb
352
- - smart_domain-0.1.0.gem
353
352
  - smart_domain.gemspec
354
353
  homepage: https://github.com/rachid/smart_domain
355
354
  licenses:
@@ -358,6 +357,7 @@ metadata:
358
357
  homepage_uri: https://github.com/rachid/smart_domain
359
358
  source_code_uri: https://github.com/rachid/smart_domain
360
359
  changelog_uri: https://github.com/rachid/smart_domain/blob/main/CHANGELOG.md
360
+ rubygems_mfa_required: 'true'
361
361
  post_install_message:
362
362
  rdoc_options: []
363
363
  require_paths:
@@ -1 +0,0 @@
1
- c993036f99084b62923212c97ff582b8
Binary file