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,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ class BaseService
5
+ # Defines the bulk result/value contract.
6
+ # @api private
7
+ module BulkContract
8
+ private
9
+
10
+ def bulk_value(operation:, items:, results:, transaction_mode:)
11
+ item_payloads =
12
+ items.zip(results).each_with_index.map do |(item, result), index|
13
+ bulk_item_payload(item:, result:, index:)
14
+ end
15
+
16
+ {
17
+ operation: operation.to_s,
18
+ transaction_mode: transaction_mode.to_s,
19
+ items: item_payloads,
20
+ summary: bulk_summary(results)
21
+ }
22
+ end
23
+
24
+ def bulk_item_payload(item:, result:, index:)
25
+ {
26
+ index:,
27
+ input: item,
28
+ success: result.success?,
29
+ value: result.success? ? result.value : nil,
30
+ error: result.failure? ? result.error.to_h : nil
31
+ }
32
+ end
33
+
34
+ def bulk_summary(results)
35
+ success_count = results.count(&:success?)
36
+ failure_count = results.count(&:failure?)
37
+
38
+ {
39
+ total: results.size,
40
+ success_count:,
41
+ failure_count:,
42
+ all_succeeded: failure_count.zero?
43
+ }
44
+ end
45
+
46
+ def bulk_meta(model_klass, operation:, transaction_mode:, limit:)
47
+ {
48
+ model: model_klass.name,
49
+ operation: operation.to_s,
50
+ transaction_mode: transaction_mode.to_s,
51
+ limit:
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ class BaseService
5
+ # Implements bulk execution strategies (transaction modes, limits).
6
+ # @api private
7
+ module BulkExecution
8
+ private
9
+
10
+ # This project targets Ruby versions where anonymous block forwarding (`&`) may be unavailable.
11
+ # rubocop:disable Style/ArgumentsForwarding
12
+ def apply_bulk_operation(model_klass, operation:, items:, transaction_mode:, &block)
13
+ limit = bulk_limit
14
+ return bulk_limit_exceeded_result(limit:, count: items.size) if items.size > limit
15
+
16
+ results = apply_bulk_results(model_klass, items, transaction_mode:, &block)
17
+
18
+ Result.success(
19
+ value: bulk_value(operation:, items:, results:, transaction_mode:),
20
+ meta: bulk_meta(model_klass, operation:, transaction_mode:, limit:)
21
+ )
22
+ end
23
+ # rubocop:enable Style/ArgumentsForwarding
24
+
25
+ # rubocop:disable Style/ArgumentsForwarding
26
+ def apply_bulk_results(model_klass, items, transaction_mode:, &block)
27
+ return apply_bulk_all_or_nothing(model_klass, items, &block) if transaction_mode == :all_or_nothing
28
+
29
+ apply_bulk_best_effort(model_klass, items, &block)
30
+ end
31
+ # rubocop:enable Style/ArgumentsForwarding
32
+
33
+ def apply_bulk_best_effort(model_klass, items, &block)
34
+ bulk_map(items) do |item|
35
+ with_transaction(model_klass) { block.call(item) }
36
+ end
37
+ end
38
+
39
+ def apply_bulk_all_or_nothing(model_klass, items, &block)
40
+ results = nil
41
+ transaction_wrapper(model_klass) do
42
+ results = bulk_map(items) { |item| block.call(item) }
43
+ rollback_transaction if results.any?(&:failure?)
44
+ end
45
+ results
46
+ end
47
+
48
+ def bulk_map(items)
49
+ results = []
50
+ items.each_slice(bulk_batch_size) do |slice|
51
+ slice.each do |item|
52
+ results << yield(item)
53
+ end
54
+ end
55
+ results
56
+ end
57
+
58
+ def bulk_limit_exceeded_result(limit:, count:)
59
+ Result.failure(
60
+ error: Errors.validation_error(
61
+ message: "Bulk limit exceeded",
62
+ details: { limit:, count: }
63
+ )
64
+ )
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ class BaseService
5
+ # Parses and normalizes bulk action params.
6
+ # @api private
7
+ module BulkParams
8
+ DEFAULT_BULK_LIMIT = 1000
9
+ DEFAULT_BATCH_SIZE = 100
10
+ TRANSACTION_MODE_ALL_OR_NOTHING = :all_or_nothing
11
+ TRANSACTION_MODE_BEST_EFFORT = :best_effort
12
+
13
+ private
14
+
15
+ def bulk_items
16
+ return [] unless params.is_a?(Hash)
17
+
18
+ items = params[:items]
19
+ return items if items.is_a?(Array)
20
+
21
+ []
22
+ end
23
+
24
+ def bulk_limit
25
+ return DEFAULT_BULK_LIMIT unless params.is_a?(Hash)
26
+
27
+ configured = params[:limit]
28
+ return DEFAULT_BULK_LIMIT unless configured.is_a?(Integer)
29
+ return DEFAULT_BULK_LIMIT if configured <= 0
30
+
31
+ configured
32
+ end
33
+
34
+ def bulk_batch_size
35
+ return DEFAULT_BATCH_SIZE unless params.is_a?(Hash)
36
+
37
+ configured = params[:batch_size]
38
+ return DEFAULT_BATCH_SIZE unless configured.is_a?(Integer)
39
+ return DEFAULT_BATCH_SIZE if configured <= 0
40
+
41
+ configured
42
+ end
43
+
44
+ def bulk_transaction_mode
45
+ return TRANSACTION_MODE_ALL_OR_NOTHING unless params.is_a?(Hash)
46
+
47
+ mode = params[:transaction_mode]
48
+ mode = mode.to_sym if mode.respond_to?(:to_sym)
49
+
50
+ return mode if [TRANSACTION_MODE_ALL_OR_NOTHING, TRANSACTION_MODE_BEST_EFFORT].include?(mode)
51
+
52
+ TRANSACTION_MODE_ALL_OR_NOTHING
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ class BaseService
5
+ # Default `create`/`update`/`destroy` action implementations.
6
+ # @api private
7
+ module CrudActions
8
+ def create
9
+ model_klass = model_class
10
+ return missing_model_class_result unless model_klass
11
+
12
+ with_transaction(model_klass) do
13
+ record = build_record(model_klass, sanitize_attributes(attributes_params))
14
+ persist_write(record, method_name: :save)
15
+ end
16
+ end
17
+
18
+ def update
19
+ model_klass = model_class
20
+ return missing_model_class_result unless model_klass
21
+
22
+ with_transaction(model_klass) do
23
+ record_result = find_record(model_klass, record_id)
24
+ return record_result if record_result.failure?
25
+
26
+ record = record_result.value
27
+ assign_attributes(record, sanitize_attributes(attributes_params))
28
+ persist_write(record, method_name: :save)
29
+ end
30
+ end
31
+
32
+ def destroy
33
+ model_klass = model_class
34
+ return missing_model_class_result unless model_klass
35
+
36
+ with_transaction(model_klass) do
37
+ record_result = find_record(model_klass, record_id)
38
+ return record_result if record_result.failure?
39
+
40
+ persist_write(record_result.value, method_name: :destroy)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def persist_write(record, method_name:)
47
+ record.public_send(method_name)
48
+
49
+ return Result.success(value: record) if write_succeeded?(record, method_name: method_name)
50
+
51
+ Result.failure(error: validation_error_for_record(record))
52
+ rescue StandardError => e
53
+ Result.failure(error: map_exception_to_error(e))
54
+ end
55
+
56
+ def write_succeeded?(record, method_name:)
57
+ return record.destroyed? || record.errors.empty? if method_name == :destroy
58
+
59
+ record.errors.empty?
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ class BaseService
5
+ # Maps common persistence exceptions to Railsmith error payloads.
6
+ # @api private
7
+ module CrudErrorMapping
8
+ private
9
+
10
+ def validation_error_for_record(record)
11
+ details =
12
+ if record.respond_to?(:errors) && record.errors.respond_to?(:to_hash)
13
+ { errors: record.errors.to_hash(true) }
14
+ else
15
+ { errors: { base: ["Validation failed"] } }
16
+ end
17
+
18
+ Errors.validation_error(details:)
19
+ end
20
+
21
+ def map_exception_to_error(exception)
22
+ mapped = map_active_record_exception(exception)
23
+ return mapped unless mapped.nil?
24
+
25
+ Errors.unexpected(details: { exception_class: exception.class.name, message: exception.message })
26
+ end
27
+
28
+ def map_active_record_exception(exception)
29
+ return nil unless defined?(ActiveRecord)
30
+
31
+ not_found_error(exception) ||
32
+ record_invalid_error(exception) ||
33
+ not_unique_error(exception) ||
34
+ stale_object_error(exception)
35
+ end
36
+
37
+ def not_found_error(exception)
38
+ return nil unless defined?(ActiveRecord::RecordNotFound)
39
+ return nil unless exception.is_a?(ActiveRecord::RecordNotFound)
40
+
41
+ Errors.not_found(message: "Record not found", details: { message: exception.message })
42
+ end
43
+
44
+ def record_invalid_error(exception)
45
+ return nil unless defined?(ActiveRecord::RecordInvalid)
46
+ return nil unless exception.is_a?(ActiveRecord::RecordInvalid)
47
+
48
+ record = exception.record
49
+ return nil if record.nil?
50
+
51
+ validation_error_for_record(record)
52
+ end
53
+
54
+ def not_unique_error(exception)
55
+ return nil unless defined?(ActiveRecord::RecordNotUnique)
56
+ return nil unless exception.is_a?(ActiveRecord::RecordNotUnique)
57
+
58
+ Errors.conflict(message: "Conflict", details: { message: exception.message })
59
+ end
60
+
61
+ def stale_object_error(exception)
62
+ return nil unless defined?(ActiveRecord::StaleObjectError)
63
+ return nil unless exception.is_a?(ActiveRecord::StaleObjectError)
64
+
65
+ Errors.conflict(message: "Conflict", details: { message: exception.message })
66
+ end
67
+
68
+ def missing_model_class_result
69
+ Result.failure(
70
+ error: Errors.validation_error(
71
+ message: "Model class not configured",
72
+ details: { service: self.class.name }
73
+ )
74
+ )
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ class BaseService
5
+ # Model resolution helpers for CRUD defaults.
6
+ # @api private
7
+ module CrudModelResolution
8
+ private
9
+
10
+ def model_class
11
+ explicit = self.class.model
12
+ return explicit unless explicit.nil?
13
+
14
+ infer_model_class
15
+ end
16
+
17
+ def infer_model_class
18
+ return nil unless self.class.name
19
+
20
+ name = self.class.name.to_s
21
+ return nil unless name.end_with?("Service")
22
+
23
+ safe_constantize(name.delete_suffix("Service"))
24
+ end
25
+
26
+ def safe_constantize(constant_name)
27
+ return constant_name.constantize if constant_name.respond_to?(:constantize)
28
+ return nil unless defined?(ActiveSupport::Inflector)
29
+
30
+ ActiveSupport::Inflector.safe_constantize(constant_name.to_s)
31
+ rescue NameError
32
+ nil
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ class BaseService
5
+ # Record building and lookup helpers for CRUD defaults.
6
+ # @api private
7
+ module CrudRecordHelpers
8
+ private
9
+
10
+ def attributes_params
11
+ return params.fetch(:attributes) if params.is_a?(Hash) && params[:attributes].is_a?(Hash)
12
+ return params if params.is_a?(Hash)
13
+
14
+ {}
15
+ end
16
+
17
+ def sanitize_attributes(attributes)
18
+ attributes
19
+ end
20
+
21
+ def record_id
22
+ return params[:id] if params.is_a?(Hash) && params.key?(:id)
23
+
24
+ nil
25
+ end
26
+
27
+ def find_record(model_klass, id)
28
+ return missing_id_result if id.nil?
29
+
30
+ record = model_klass.find_by(id:)
31
+ return Result.success(value: record) unless record.nil?
32
+
33
+ not_found_result(model_klass, id)
34
+ rescue StandardError => e
35
+ Result.failure(error: map_exception_to_error(e))
36
+ end
37
+
38
+ def build_record(model_klass, attributes)
39
+ model_klass.new(attributes)
40
+ end
41
+
42
+ def assign_attributes(record, attributes)
43
+ record.assign_attributes(attributes)
44
+ end
45
+
46
+ def missing_id_result
47
+ Result.failure(error: Errors.validation_error(details: { missing: ["id"] }))
48
+ end
49
+
50
+ def not_found_result(model_klass, id)
51
+ Result.failure(
52
+ error: Errors.not_found(
53
+ message: "Record not found",
54
+ details: { model: model_klass.name, id: }
55
+ )
56
+ )
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ class BaseService
5
+ # Transaction helpers for write-path actions.
6
+ # @api private
7
+ module CrudTransactions
8
+ private
9
+
10
+ def with_transaction(model_klass)
11
+ result = nil
12
+ transaction_wrapper(model_klass) do
13
+ result = yield
14
+ rollback_transaction if result.is_a?(Result) && result.failure?
15
+ end
16
+ result
17
+ end
18
+
19
+ def transaction_wrapper(model_klass, &)
20
+ return model_klass.transaction(&) if model_klass.respond_to?(:transaction)
21
+ return ActiveRecord::Base.transaction(&) if defined?(ActiveRecord::Base)
22
+
23
+ yield
24
+ end
25
+
26
+ def rollback_transaction
27
+ raise ActiveRecord::Rollback if defined?(ActiveRecord::Rollback)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ class BaseService
5
+ # Exposes the current domain from context and emits instrumentation events
6
+ # so domain tags flow into observability tooling on every service call.
7
+ # @api private
8
+ module DomainContextPropagation
9
+ # Returns the domain key from the service context, or nil when not set.
10
+ # A nil domain is permitted in flexible mode.
11
+ def current_domain
12
+ DomainContext.normalize_current_domain(context[:current_domain])
13
+ end
14
+
15
+ private
16
+
17
+ # Wraps action execution with a domain-tagged instrumentation event.
18
+ def execute_action(action:)
19
+ Railsmith::CrossDomainGuard.emit_if_violation(instance: self, action: action)
20
+ Instrumentation.instrument(
21
+ "service.call",
22
+ service: self.class.name,
23
+ action: action,
24
+ domain: current_domain
25
+ ) { super }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ class BaseService
5
+ # Deep-duplication helpers for params/context immutability.
6
+ # @api private
7
+ module DupHelpers
8
+ private
9
+
10
+ def deep_dup(value)
11
+ Railsmith.deep_dup(value)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ class BaseService
5
+ # Parameter validation helpers for service actions.
6
+ module Validation
7
+ # Explicit validation helper intended to be called from action methods.
8
+ #
9
+ # Supports either:
10
+ # - required_keys: simple presence checks on Hash-like params
11
+ # - contract: a dry-validation-like contract responding to `call(input)` and returning
12
+ # an object that responds to `success?` and `errors`
13
+ def validate(input = params, required_keys: [], contract: nil)
14
+ return validate_with_contract(contract, input) if contract
15
+
16
+ validate_required_keys(input, required_keys)
17
+ end
18
+
19
+ private
20
+
21
+ def validate_required_keys(input, required_keys)
22
+ hash = input.is_a?(Hash) ? input : {}
23
+ missing = required_keys.reject { |key| present_value?(hash[key]) }
24
+
25
+ return Result.success(value: hash) if missing.empty?
26
+
27
+ Result.failure(
28
+ error: Errors.validation_error(
29
+ message: "Validation failed",
30
+ details: { missing: missing.map(&:to_s) }
31
+ )
32
+ )
33
+ end
34
+
35
+ def validate_with_contract(contract, input)
36
+ result = contract.call(input)
37
+ return Result.success(value: input) if contract_success?(result)
38
+
39
+ Result.failure(
40
+ error: Errors.validation_error(
41
+ message: "Validation failed",
42
+ details: { errors: contract_errors(result) }
43
+ )
44
+ )
45
+ rescue StandardError => e
46
+ Result.failure(error: Errors.unexpected(message: e.message))
47
+ end
48
+
49
+ def contract_success?(result)
50
+ result.respond_to?(:success?) && result.success?
51
+ end
52
+
53
+ def contract_errors(result)
54
+ return result.errors if result.respond_to?(:errors)
55
+
56
+ { base: ["Invalid contract result"] }
57
+ end
58
+
59
+ def present_value?(value)
60
+ return false if value.nil?
61
+ return false if value.respond_to?(:empty?) && value.empty?
62
+
63
+ true
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ # Base service entrypoint with explicit (non-hook) lifecycle.
5
+ class BaseService
6
+ require_relative "base_service/dup_helpers"
7
+ require_relative "base_service/validation"
8
+ require_relative "base_service/crud_actions"
9
+ require_relative "base_service/bulk_params"
10
+ require_relative "base_service/bulk_execution"
11
+ require_relative "base_service/bulk_contract"
12
+ require_relative "base_service/bulk_actions"
13
+ require_relative "base_service/crud_model_resolution"
14
+ require_relative "base_service/crud_record_helpers"
15
+ require_relative "base_service/crud_error_mapping"
16
+ require_relative "base_service/crud_transactions"
17
+ require_relative "base_service/domain_context_propagation"
18
+ include DupHelpers
19
+ include Validation
20
+ include CrudActions
21
+ include BulkActions
22
+ prepend DomainContextPropagation
23
+
24
+ include CrudModelResolution
25
+ include CrudRecordHelpers
26
+ include CrudErrorMapping
27
+ include CrudTransactions
28
+
29
+ class << self
30
+ def call(action:, params: {}, context: {})
31
+ new(params:, context:).call(action:)
32
+ end
33
+
34
+ def model(model_class = nil)
35
+ return @model_class if model_class.nil?
36
+
37
+ @model_class = model_class
38
+ end
39
+
40
+ # Bounded-context key for this service (optional). When set, mismatches
41
+ # against +context[:current_domain]+ emit warn-only instrumentation unless
42
+ # the pair is listed in +Railsmith.configuration.cross_domain_allowlist+.
43
+ def service_domain(domain_key = nil)
44
+ return @service_domain if domain_key.nil?
45
+
46
+ @service_domain = DomainContext.normalize_current_domain(domain_key)
47
+ end
48
+ end
49
+
50
+ attr_reader :params, :context
51
+
52
+ def initialize(params:, context:)
53
+ @params = deep_dup(params || {})
54
+ @context = deep_dup(context || {})
55
+ end
56
+
57
+ def call(action:)
58
+ normalized_action = normalize_action(action)
59
+ return invalid_action_result(action: normalized_action) unless valid_action?(normalized_action)
60
+
61
+ result = execute_action(action: normalized_action)
62
+ normalize_result(result)
63
+ end
64
+
65
+ private
66
+
67
+ def execute_action(action:)
68
+ public_send(action)
69
+ end
70
+
71
+ def normalize_result(value)
72
+ return value if value.is_a?(Result)
73
+
74
+ Result.success(value:)
75
+ end
76
+
77
+ def valid_action?(action)
78
+ action.is_a?(Symbol) && respond_to?(action, true)
79
+ end
80
+
81
+ def normalize_action(action)
82
+ return action.to_sym if action.respond_to?(:to_sym)
83
+
84
+ action
85
+ end
86
+
87
+ def invalid_action_result(action:)
88
+ Result.failure(
89
+ error: Errors.validation_error(
90
+ message: "Invalid action",
91
+ details: { action: action }
92
+ )
93
+ )
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Railsmith
4
+ # Stores global settings used by gem components.
5
+ class Configuration
6
+ attr_accessor :warn_on_cross_domain_calls, :strict_mode,
7
+ :cross_domain_allowlist, :on_cross_domain_violation,
8
+ :fail_on_arch_violations
9
+
10
+ def initialize
11
+ @warn_on_cross_domain_calls = true
12
+ @strict_mode = false
13
+ @cross_domain_allowlist = []
14
+ @on_cross_domain_violation = nil
15
+ @fail_on_arch_violations = false
16
+ end
17
+ end
18
+ end