railsmith 1.0.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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/.tool-versions +1 -0
  3. data/CHANGELOG.md +64 -0
  4. data/LICENSE.txt +21 -0
  5. data/MIGRATION.md +156 -0
  6. data/README.md +249 -0
  7. data/Rakefile +14 -0
  8. data/docs/cookbook.md +605 -0
  9. data/docs/legacy-adoption.md +283 -0
  10. data/docs/quickstart.md +110 -0
  11. data/lib/generators/railsmith/domain/domain_generator.rb +57 -0
  12. data/lib/generators/railsmith/domain/templates/domain.rb.tt +14 -0
  13. data/lib/generators/railsmith/install/install_generator.rb +21 -0
  14. data/lib/generators/railsmith/install/templates/railsmith.rb +10 -0
  15. data/lib/generators/railsmith/model_service/model_service_generator.rb +121 -0
  16. data/lib/generators/railsmith/model_service/templates/model_service.rb.tt +28 -0
  17. data/lib/generators/railsmith/operation/operation_generator.rb +88 -0
  18. data/lib/generators/railsmith/operation/templates/operation.rb.tt +27 -0
  19. data/lib/railsmith/arch_checks/cli.rb +79 -0
  20. data/lib/railsmith/arch_checks/direct_model_access_checker.rb +94 -0
  21. data/lib/railsmith/arch_checks/missing_service_usage_checker.rb +206 -0
  22. data/lib/railsmith/arch_checks/violation.rb +14 -0
  23. data/lib/railsmith/arch_checks.rb +7 -0
  24. data/lib/railsmith/arch_report.rb +96 -0
  25. data/lib/railsmith/base_service/bulk_actions.rb +77 -0
  26. data/lib/railsmith/base_service/bulk_contract.rb +56 -0
  27. data/lib/railsmith/base_service/bulk_execution.rb +68 -0
  28. data/lib/railsmith/base_service/bulk_params.rb +56 -0
  29. data/lib/railsmith/base_service/crud_actions.rb +63 -0
  30. data/lib/railsmith/base_service/crud_error_mapping.rb +78 -0
  31. data/lib/railsmith/base_service/crud_model_resolution.rb +36 -0
  32. data/lib/railsmith/base_service/crud_record_helpers.rb +60 -0
  33. data/lib/railsmith/base_service/crud_transactions.rb +31 -0
  34. data/lib/railsmith/base_service/domain_context_propagation.rb +29 -0
  35. data/lib/railsmith/base_service/dup_helpers.rb +15 -0
  36. data/lib/railsmith/base_service/validation.rb +67 -0
  37. data/lib/railsmith/base_service.rb +96 -0
  38. data/lib/railsmith/configuration.rb +18 -0
  39. data/lib/railsmith/cross_domain_guard.rb +90 -0
  40. data/lib/railsmith/cross_domain_warning_formatter.rb +66 -0
  41. data/lib/railsmith/deep_dup.rb +20 -0
  42. data/lib/railsmith/domain_context.rb +44 -0
  43. data/lib/railsmith/errors.rb +50 -0
  44. data/lib/railsmith/instrumentation.rb +64 -0
  45. data/lib/railsmith/railtie.rb +10 -0
  46. data/lib/railsmith/result.rb +60 -0
  47. data/lib/railsmith/version.rb +5 -0
  48. data/lib/railsmith.rb +31 -0
  49. data/lib/tasks/railsmith.rake +24 -0
  50. data/sig/railsmith.rbs +4 -0
  51. metadata +116 -0
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ # Detects when a service from one bounded context runs under another domain's
5
+ # request context (+context[:current_domain]+). Emits non-blocking
6
+ # +cross_domain.warning.railsmith+ instrumentation by default; optional strict
7
+ # hook runs when +strict_mode+ is enabled.
8
+ module CrossDomainGuard
9
+ def self.emit_if_violation(instance:, action:, configuration: Railsmith.configuration)
10
+ return unless configuration.warn_on_cross_domain_calls
11
+
12
+ mismatch = domain_mismatch(instance)
13
+ return if mismatch.nil? || allowlisted?(configuration, mismatch)
14
+
15
+ publish_violation(instance:, action:, configuration:, mismatch:)
16
+ end
17
+
18
+ def self.allowlisted?(configuration, mismatch)
19
+ allowed_crossing?(
20
+ configuration.cross_domain_allowlist,
21
+ mismatch[:context_domain],
22
+ mismatch[:service_domain]
23
+ )
24
+ end
25
+
26
+ def self.publish_violation(instance:, action:, configuration:, mismatch:)
27
+ payload = build_payload(
28
+ context_domain: mismatch[:context_domain],
29
+ service_domain: mismatch[:service_domain],
30
+ service: instance.class.name,
31
+ action: action,
32
+ strict_mode: configuration.strict_mode
33
+ )
34
+
35
+ Instrumentation.instrument("cross_domain.warning", payload)
36
+ configuration.on_cross_domain_violation&.call(payload) if configuration.strict_mode
37
+ end
38
+
39
+ def self.domain_mismatch(instance)
40
+ context_domain = DomainContext.normalize_current_domain(instance.context[:current_domain])
41
+ service_domain = instance.class.service_domain
42
+ return nil if context_domain.nil? || service_domain.nil?
43
+ return nil if context_domain == service_domain
44
+
45
+ { context_domain: context_domain, service_domain: service_domain }
46
+ end
47
+
48
+ def self.allowed_crossing?(allowlist, from_domain, to_domain)
49
+ Array(allowlist).any? { |entry| pair_matches?(entry, from_domain, to_domain) }
50
+ end
51
+
52
+ def self.pair_matches?(entry, from_domain, to_domain)
53
+ case entry
54
+ when Hash
55
+ hash_pair_matches?(entry, from_domain, to_domain)
56
+ when Array
57
+ array_pair_matches?(entry, from_domain, to_domain)
58
+ else
59
+ false
60
+ end
61
+ end
62
+
63
+ def self.hash_pair_matches?(entry, from_domain, to_domain)
64
+ from_key = entry[:from] || entry["from"]
65
+ to_key = entry[:to] || entry["to"]
66
+ DomainContext.normalize_current_domain(from_key) == from_domain &&
67
+ DomainContext.normalize_current_domain(to_key) == to_domain
68
+ end
69
+
70
+ def self.array_pair_matches?(entry, from_domain, to_domain)
71
+ return false unless entry.size == 2
72
+
73
+ DomainContext.normalize_current_domain(entry[0]) == from_domain &&
74
+ DomainContext.normalize_current_domain(entry[1]) == to_domain
75
+ end
76
+
77
+ def self.build_payload(context_domain:, service_domain:, service:, action:, strict_mode:)
78
+ {
79
+ event: "cross_domain.warning",
80
+ context_domain: context_domain,
81
+ service_domain: service_domain,
82
+ service: service,
83
+ action: action,
84
+ strict_mode: strict_mode,
85
+ blocking: false,
86
+ occurred_at: Time.now.utc.iso8601(6)
87
+ }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Railsmith
6
+ # Stable, log- and CI-friendly renderings of cross-domain warning payloads.
7
+ module CrossDomainWarningFormatter
8
+ module_function
9
+
10
+ CANONICAL_KEYS = %i[
11
+ event
12
+ context_domain
13
+ service_domain
14
+ service
15
+ action
16
+ strict_mode
17
+ blocking
18
+ occurred_at
19
+ ].freeze
20
+
21
+ # Single-line JSON with sorted keys for grep and log aggregation.
22
+ def as_json_line(payload)
23
+ JSON.generate(ordered_hash(payload))
24
+ end
25
+
26
+ # Space-separated key=value for quick human scanning (values are JSON-encoded).
27
+ def as_key_value_line(payload)
28
+ (canonical_kv_parts(payload) + extra_kv_parts(payload)).join(" ")
29
+ end
30
+
31
+ def ordered_hash(payload)
32
+ ordered = CANONICAL_KEYS.each_with_object({}) do |key, acc|
33
+ value = payload[key]
34
+ acc[key.to_s] = json_scalar(value) unless value.nil?
35
+ end
36
+ payload.each do |key, value|
37
+ string_key = key.to_s
38
+ ordered[string_key] = json_scalar(value) unless ordered.key?(string_key) || value.nil?
39
+ end
40
+ ordered
41
+ end
42
+
43
+ def canonical_kv_parts(payload)
44
+ CANONICAL_KEYS.filter_map do |key|
45
+ next if payload[key].nil?
46
+
47
+ %(#{key}=#{JSON.generate(json_scalar(payload[key]))})
48
+ end
49
+ end
50
+
51
+ def extra_kv_parts(payload)
52
+ (payload.keys - CANONICAL_KEYS).sort.filter_map do |key|
53
+ value = payload[key]
54
+ next if value.nil?
55
+
56
+ %(#{key}=#{JSON.generate(json_scalar(value))})
57
+ end
58
+ end
59
+
60
+ def json_scalar(value)
61
+ value.is_a?(Symbol) ? value.to_s : value
62
+ end
63
+
64
+ private_class_method :ordered_hash, :canonical_kv_parts, :extra_kv_parts, :json_scalar
65
+ end
66
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Railsmith — lightweight service/operation framework for Rails.
4
+ module Railsmith
5
+ module_function
6
+
7
+ # Deep-duplicates Hash/Array trees for defensive copies (params/context).
8
+ def deep_dup(value)
9
+ case value
10
+ when Hash
11
+ value.each_with_object({}) { |(key, item), memo| memo[key] = deep_dup(item) }
12
+ when Array
13
+ value.map { |item| deep_dup(item) }
14
+ else
15
+ value.dup
16
+ end
17
+ rescue TypeError
18
+ value
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ # Explicit, immutable value object for domain context propagation.
5
+ # Build one per request and pass it as `context` into service/operation calls.
6
+ #
7
+ # Example:
8
+ # ctx = Railsmith::DomainContext.new(current_domain: :billing, meta: { request_id: "abc" })
9
+ # BillingService.call(action: :create, params: params, context: ctx.to_h)
10
+ class DomainContext
11
+ attr_reader :current_domain, :meta
12
+
13
+ def self.normalize_current_domain(value)
14
+ return nil if value.nil?
15
+ return nil if value.is_a?(String) && value.strip.empty?
16
+ return value if value.is_a?(Symbol)
17
+
18
+ value.respond_to?(:to_sym) ? value.to_sym : value
19
+ end
20
+
21
+ def initialize(current_domain: nil, meta: {})
22
+ @current_domain = self.class.normalize_current_domain(current_domain)
23
+ @meta = (meta || {}).freeze
24
+ freeze
25
+ end
26
+
27
+ # Serializes to a plain hash suitable for passing into service context.
28
+ # +current_domain+ is authoritative; meta cannot replace it.
29
+ def to_h
30
+ extras =
31
+ if meta.empty?
32
+ {}
33
+ else
34
+ meta.except(:current_domain, "current_domain")
35
+ end
36
+ { current_domain: current_domain }.merge(extras)
37
+ end
38
+
39
+ # Returns true when no domain has been set (allowed in flexible mode).
40
+ def blank_domain?
41
+ current_domain.nil?
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ # Normalized builders for failure payloads.
5
+ module Errors
6
+ # A structured error payload used in failure results.
7
+ class ErrorPayload
8
+ attr_reader :code, :message, :details
9
+
10
+ def initialize(code:, message:, details: nil)
11
+ @code = code.to_s
12
+ @message = message.to_s
13
+ @details = details
14
+ freeze
15
+ end
16
+
17
+ def to_h
18
+ payload = { code:, message: }
19
+ payload[:details] = details unless details.nil?
20
+ payload
21
+ end
22
+
23
+ def as_json(*)
24
+ to_h
25
+ end
26
+ end
27
+
28
+ class << self
29
+ def validation_error(message: "Validation failed", details: nil)
30
+ ErrorPayload.new(code: :validation_error, message:, details: details || {})
31
+ end
32
+
33
+ def not_found(message: "Not found", details: nil)
34
+ ErrorPayload.new(code: :not_found, message:, details: details || {})
35
+ end
36
+
37
+ def conflict(message: "Conflict", details: nil)
38
+ ErrorPayload.new(code: :conflict, message:, details: details || {})
39
+ end
40
+
41
+ def unauthorized(message: "Unauthorized", details: nil)
42
+ ErrorPayload.new(code: :unauthorized, message:, details: details || {})
43
+ end
44
+
45
+ def unexpected(message: "Unexpected error", details: nil)
46
+ ErrorPayload.new(code: :unexpected, message:, details: details)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ # Lightweight instrumentation hook layer for domain-tagged service events.
5
+ #
6
+ # Uses ActiveSupport::Notifications when available so events slot naturally
7
+ # into Rails instrumentation pipelines. Falls back to plain Ruby subscribers
8
+ # for non-Rails contexts.
9
+ #
10
+ # Example (plain Ruby subscriber):
11
+ # Railsmith::Instrumentation.subscribe("service.call") do |event, payload|
12
+ # Rails.logger.info "[#{payload[:domain]}] #{payload[:service]}##{payload[:action]}"
13
+ # end
14
+ module Instrumentation
15
+ EVENT_NAMESPACE = "railsmith"
16
+
17
+ class << self
18
+ # Emit a domain-tagged event, yielding to the wrapped block if given.
19
+ # Payload is always a Hash; a :domain key is expected for domain tagging.
20
+ # Always dispatches to plain Ruby subscribers; also emits to
21
+ # ActiveSupport::Notifications when available for Rails integration.
22
+ def instrument(event_name, payload = {}, &block)
23
+ full_name = "#{event_name}.#{EVENT_NAMESPACE}"
24
+ result = nil
25
+ if active_support_notifications?
26
+ ActiveSupport::Notifications.instrument(full_name, payload) { result = block&.call }
27
+ else
28
+ result = block&.call
29
+ end
30
+ dispatch(full_name, payload)
31
+ result
32
+ end
33
+
34
+ # Register a plain Ruby subscriber for events matching an optional prefix.
35
+ # Subscriber is called with (event_name, payload).
36
+ def subscribe(pattern = nil, &block)
37
+ subscribers << { pattern: pattern, handler: block }
38
+ end
39
+
40
+ # Remove all plain Ruby subscribers (useful in tests).
41
+ def reset!
42
+ @subscribers = []
43
+ end
44
+
45
+ private
46
+
47
+ def subscribers
48
+ @subscribers ||= []
49
+ end
50
+
51
+ def active_support_notifications?
52
+ defined?(ActiveSupport::Notifications)
53
+ end
54
+
55
+ def dispatch(event_name, payload)
56
+ subscribers.each do |sub|
57
+ next if sub[:pattern] && !event_name.start_with?(sub[:pattern])
58
+
59
+ sub[:handler].call(event_name, payload)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ # Loads Rake tasks when the gem is used inside a Rails application.
5
+ class Railtie < ::Rails::Railtie
6
+ rake_tasks do
7
+ load File.expand_path("../tasks/railsmith.rake", __dir__)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ # An immutable success/failure wrapper with a stable serialization contract.
5
+ class Result
6
+ def self.success(value: nil, meta: nil)
7
+ new(success: true, value:, error: nil, meta: meta || {})
8
+ end
9
+
10
+ def self.failure(code: nil, message: nil, details: nil, error: nil, meta: nil)
11
+ normalized_error =
12
+ error ||
13
+ Errors::ErrorPayload.new(
14
+ code: code || :unexpected,
15
+ message: message || "Unexpected error",
16
+ details:
17
+ )
18
+
19
+ new(success: false, value: nil, error: normalized_error, meta: meta || {})
20
+ end
21
+
22
+ private_class_method :new
23
+
24
+ def initialize(success:, value:, error:, meta:)
25
+ @success = success ? true : false
26
+ @value = value
27
+ @error = error
28
+ @meta = meta || {}
29
+ freeze
30
+ end
31
+
32
+ def success?
33
+ @success
34
+ end
35
+
36
+ def failure?
37
+ !success?
38
+ end
39
+
40
+ attr_reader :value, :error, :meta
41
+
42
+ def code
43
+ return nil if error.nil?
44
+
45
+ error.code
46
+ end
47
+
48
+ def to_h
49
+ if success?
50
+ { success: true, value:, meta: }
51
+ else
52
+ { success: false, error: error.to_h, meta: }
53
+ end
54
+ end
55
+
56
+ def as_json(*)
57
+ to_h
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ VERSION = "1.0.0"
5
+ end
data/lib/railsmith.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "railsmith/version"
4
+ require_relative "railsmith/configuration"
5
+ require_relative "railsmith/errors"
6
+ require_relative "railsmith/result"
7
+ require_relative "railsmith/deep_dup"
8
+ require_relative "railsmith/domain_context"
9
+ require_relative "railsmith/instrumentation"
10
+ require_relative "railsmith/cross_domain_guard"
11
+ require_relative "railsmith/cross_domain_warning_formatter"
12
+ require_relative "railsmith/base_service"
13
+
14
+ require_relative "railsmith/railtie" if defined?(Rails::Railtie)
15
+
16
+ # Entry point for global gem configuration and loading.
17
+ module Railsmith
18
+ class Error < StandardError; end
19
+
20
+ class << self
21
+ attr_writer :configuration
22
+
23
+ def configuration
24
+ @configuration ||= Configuration.new
25
+ end
26
+
27
+ def configure
28
+ yield(configuration)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :railsmith do
4
+ desc <<~DESC
5
+ Run Railsmith architecture checks on controller files and print a report.
6
+
7
+ Configuration via environment variables:
8
+ RAILSMITH_PATHS Comma-separated controller directories (default: app/controllers)
9
+ RAILSMITH_FORMAT Output format: "text" or "json" (default: text; invalid values fall back to text with a warning)
10
+ RAILSMITH_FAIL_ON_ARCH_VIOLATIONS If set to "true", "1", or "yes", exit 1 when violations exist (overrides config)
11
+
12
+ Exit behaviour:
13
+ Exits 0 in warn-only mode (the default) regardless of violations.
14
+ Set +Railsmith.configuration.fail_on_arch_violations = true+ or
15
+ +RAILSMITH_FAIL_ON_ARCH_VIOLATIONS=true+ to exit 1 when violations are found.
16
+ DESC
17
+ task :arch_check do
18
+ require "railsmith"
19
+ require "railsmith/arch_checks"
20
+
21
+ status = Railsmith::ArchChecks::Cli.run
22
+ exit status unless status.zero?
23
+ end
24
+ end
data/sig/railsmith.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Railsmith
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: railsmith
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - samaswin
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-03-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '7.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9.0'
33
+ description: Railsmith provides service-layer architecture primitives for domain routing,
34
+ CRUD/bulk operations, and structured results.
35
+ email:
36
+ - samaswin@users.noreply.github.com
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - ".tool-versions"
42
+ - CHANGELOG.md
43
+ - LICENSE.txt
44
+ - MIGRATION.md
45
+ - README.md
46
+ - Rakefile
47
+ - docs/cookbook.md
48
+ - docs/legacy-adoption.md
49
+ - docs/quickstart.md
50
+ - lib/generators/railsmith/domain/domain_generator.rb
51
+ - lib/generators/railsmith/domain/templates/domain.rb.tt
52
+ - lib/generators/railsmith/install/install_generator.rb
53
+ - lib/generators/railsmith/install/templates/railsmith.rb
54
+ - lib/generators/railsmith/model_service/model_service_generator.rb
55
+ - lib/generators/railsmith/model_service/templates/model_service.rb.tt
56
+ - lib/generators/railsmith/operation/operation_generator.rb
57
+ - lib/generators/railsmith/operation/templates/operation.rb.tt
58
+ - lib/railsmith.rb
59
+ - lib/railsmith/arch_checks.rb
60
+ - lib/railsmith/arch_checks/cli.rb
61
+ - lib/railsmith/arch_checks/direct_model_access_checker.rb
62
+ - lib/railsmith/arch_checks/missing_service_usage_checker.rb
63
+ - lib/railsmith/arch_checks/violation.rb
64
+ - lib/railsmith/arch_report.rb
65
+ - lib/railsmith/base_service.rb
66
+ - lib/railsmith/base_service/bulk_actions.rb
67
+ - lib/railsmith/base_service/bulk_contract.rb
68
+ - lib/railsmith/base_service/bulk_execution.rb
69
+ - lib/railsmith/base_service/bulk_params.rb
70
+ - lib/railsmith/base_service/crud_actions.rb
71
+ - lib/railsmith/base_service/crud_error_mapping.rb
72
+ - lib/railsmith/base_service/crud_model_resolution.rb
73
+ - lib/railsmith/base_service/crud_record_helpers.rb
74
+ - lib/railsmith/base_service/crud_transactions.rb
75
+ - lib/railsmith/base_service/domain_context_propagation.rb
76
+ - lib/railsmith/base_service/dup_helpers.rb
77
+ - lib/railsmith/base_service/validation.rb
78
+ - lib/railsmith/configuration.rb
79
+ - lib/railsmith/cross_domain_guard.rb
80
+ - lib/railsmith/cross_domain_warning_formatter.rb
81
+ - lib/railsmith/deep_dup.rb
82
+ - lib/railsmith/domain_context.rb
83
+ - lib/railsmith/errors.rb
84
+ - lib/railsmith/instrumentation.rb
85
+ - lib/railsmith/railtie.rb
86
+ - lib/railsmith/result.rb
87
+ - lib/railsmith/version.rb
88
+ - lib/tasks/railsmith.rake
89
+ - sig/railsmith.rbs
90
+ homepage: https://github.com/samaswin/railsmith
91
+ licenses:
92
+ - MIT
93
+ metadata:
94
+ homepage_uri: https://github.com/samaswin/railsmith
95
+ source_code_uri: https://github.com/samaswin/railsmith
96
+ rubygems_mfa_required: 'true'
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: 3.2.0
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubygems_version: 3.5.22
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: All-in-one service layer conventions for Rails.
116
+ test_files: []