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.
- checksums.yaml +7 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +64 -0
- data/LICENSE.txt +21 -0
- data/MIGRATION.md +156 -0
- data/README.md +249 -0
- data/Rakefile +14 -0
- data/docs/cookbook.md +605 -0
- data/docs/legacy-adoption.md +283 -0
- data/docs/quickstart.md +110 -0
- data/lib/generators/railsmith/domain/domain_generator.rb +57 -0
- data/lib/generators/railsmith/domain/templates/domain.rb.tt +14 -0
- data/lib/generators/railsmith/install/install_generator.rb +21 -0
- data/lib/generators/railsmith/install/templates/railsmith.rb +10 -0
- data/lib/generators/railsmith/model_service/model_service_generator.rb +121 -0
- data/lib/generators/railsmith/model_service/templates/model_service.rb.tt +28 -0
- data/lib/generators/railsmith/operation/operation_generator.rb +88 -0
- data/lib/generators/railsmith/operation/templates/operation.rb.tt +27 -0
- data/lib/railsmith/arch_checks/cli.rb +79 -0
- data/lib/railsmith/arch_checks/direct_model_access_checker.rb +94 -0
- data/lib/railsmith/arch_checks/missing_service_usage_checker.rb +206 -0
- data/lib/railsmith/arch_checks/violation.rb +14 -0
- data/lib/railsmith/arch_checks.rb +7 -0
- data/lib/railsmith/arch_report.rb +96 -0
- data/lib/railsmith/base_service/bulk_actions.rb +77 -0
- data/lib/railsmith/base_service/bulk_contract.rb +56 -0
- data/lib/railsmith/base_service/bulk_execution.rb +68 -0
- data/lib/railsmith/base_service/bulk_params.rb +56 -0
- data/lib/railsmith/base_service/crud_actions.rb +63 -0
- data/lib/railsmith/base_service/crud_error_mapping.rb +78 -0
- data/lib/railsmith/base_service/crud_model_resolution.rb +36 -0
- data/lib/railsmith/base_service/crud_record_helpers.rb +60 -0
- data/lib/railsmith/base_service/crud_transactions.rb +31 -0
- data/lib/railsmith/base_service/domain_context_propagation.rb +29 -0
- data/lib/railsmith/base_service/dup_helpers.rb +15 -0
- data/lib/railsmith/base_service/validation.rb +67 -0
- data/lib/railsmith/base_service.rb +96 -0
- data/lib/railsmith/configuration.rb +18 -0
- data/lib/railsmith/cross_domain_guard.rb +90 -0
- data/lib/railsmith/cross_domain_warning_formatter.rb +66 -0
- data/lib/railsmith/deep_dup.rb +20 -0
- data/lib/railsmith/domain_context.rb +44 -0
- data/lib/railsmith/errors.rb +50 -0
- data/lib/railsmith/instrumentation.rb +64 -0
- data/lib/railsmith/railtie.rb +10 -0
- data/lib/railsmith/result.rb +60 -0
- data/lib/railsmith/version.rb +5 -0
- data/lib/railsmith.rb +31 -0
- data/lib/tasks/railsmith.rake +24 -0
- data/sig/railsmith.rbs +4 -0
- 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,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
|
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
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: []
|