better_service 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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1321 -0
  4. data/Rakefile +15 -0
  5. data/lib/better_service/cache_service.rb +310 -0
  6. data/lib/better_service/concerns/instrumentation.rb +242 -0
  7. data/lib/better_service/concerns/serviceable/authorizable.rb +106 -0
  8. data/lib/better_service/concerns/serviceable/cacheable.rb +97 -0
  9. data/lib/better_service/concerns/serviceable/messageable.rb +30 -0
  10. data/lib/better_service/concerns/serviceable/presentable.rb +66 -0
  11. data/lib/better_service/concerns/serviceable/transactional.rb +51 -0
  12. data/lib/better_service/concerns/serviceable/validatable.rb +58 -0
  13. data/lib/better_service/concerns/serviceable/viewable.rb +49 -0
  14. data/lib/better_service/concerns/serviceable.rb +12 -0
  15. data/lib/better_service/concerns/workflowable/callbacks.rb +116 -0
  16. data/lib/better_service/concerns/workflowable/context.rb +108 -0
  17. data/lib/better_service/concerns/workflowable/step.rb +141 -0
  18. data/lib/better_service/concerns/workflowable.rb +12 -0
  19. data/lib/better_service/configuration.rb +113 -0
  20. data/lib/better_service/errors/better_service_error.rb +271 -0
  21. data/lib/better_service/errors/configuration/configuration_error.rb +21 -0
  22. data/lib/better_service/errors/configuration/invalid_configuration_error.rb +28 -0
  23. data/lib/better_service/errors/configuration/invalid_schema_error.rb +28 -0
  24. data/lib/better_service/errors/configuration/nil_user_error.rb +37 -0
  25. data/lib/better_service/errors/configuration/schema_required_error.rb +29 -0
  26. data/lib/better_service/errors/runtime/authorization_error.rb +38 -0
  27. data/lib/better_service/errors/runtime/database_error.rb +38 -0
  28. data/lib/better_service/errors/runtime/execution_error.rb +27 -0
  29. data/lib/better_service/errors/runtime/resource_not_found_error.rb +38 -0
  30. data/lib/better_service/errors/runtime/runtime_error.rb +22 -0
  31. data/lib/better_service/errors/runtime/transaction_error.rb +34 -0
  32. data/lib/better_service/errors/runtime/validation_error.rb +42 -0
  33. data/lib/better_service/errors/workflowable/configuration/duplicate_step_error.rb +27 -0
  34. data/lib/better_service/errors/workflowable/configuration/invalid_step_error.rb +12 -0
  35. data/lib/better_service/errors/workflowable/configuration/step_not_found_error.rb +29 -0
  36. data/lib/better_service/errors/workflowable/configuration/workflow_configuration_error.rb +24 -0
  37. data/lib/better_service/errors/workflowable/runtime/rollback_error.rb +46 -0
  38. data/lib/better_service/errors/workflowable/runtime/step_execution_error.rb +47 -0
  39. data/lib/better_service/errors/workflowable/runtime/workflow_execution_error.rb +40 -0
  40. data/lib/better_service/errors/workflowable/runtime/workflow_runtime_error.rb +25 -0
  41. data/lib/better_service/railtie.rb +6 -0
  42. data/lib/better_service/services/action_service.rb +60 -0
  43. data/lib/better_service/services/base.rb +249 -0
  44. data/lib/better_service/services/create_service.rb +60 -0
  45. data/lib/better_service/services/destroy_service.rb +57 -0
  46. data/lib/better_service/services/index_service.rb +56 -0
  47. data/lib/better_service/services/show_service.rb +44 -0
  48. data/lib/better_service/services/update_service.rb +58 -0
  49. data/lib/better_service/subscribers/log_subscriber.rb +131 -0
  50. data/lib/better_service/subscribers/stats_subscriber.rb +208 -0
  51. data/lib/better_service/version.rb +3 -0
  52. data/lib/better_service/workflows/base.rb +106 -0
  53. data/lib/better_service/workflows/dsl.rb +59 -0
  54. data/lib/better_service/workflows/execution.rb +89 -0
  55. data/lib/better_service/workflows/result_builder.rb +67 -0
  56. data/lib/better_service/workflows/rollback_support.rb +44 -0
  57. data/lib/better_service/workflows/transaction_support.rb +32 -0
  58. data/lib/better_service.rb +28 -0
  59. data/lib/generators/serviceable/action_generator.rb +29 -0
  60. data/lib/generators/serviceable/create_generator.rb +27 -0
  61. data/lib/generators/serviceable/destroy_generator.rb +27 -0
  62. data/lib/generators/serviceable/index_generator.rb +27 -0
  63. data/lib/generators/serviceable/scaffold_generator.rb +70 -0
  64. data/lib/generators/serviceable/show_generator.rb +27 -0
  65. data/lib/generators/serviceable/templates/action_service.rb.tt +42 -0
  66. data/lib/generators/serviceable/templates/create_service.rb.tt +33 -0
  67. data/lib/generators/serviceable/templates/destroy_service.rb.tt +40 -0
  68. data/lib/generators/serviceable/templates/index_service.rb.tt +54 -0
  69. data/lib/generators/serviceable/templates/service_test.rb.tt +23 -0
  70. data/lib/generators/serviceable/templates/show_service.rb.tt +37 -0
  71. data/lib/generators/serviceable/templates/update_service.rb.tt +50 -0
  72. data/lib/generators/serviceable/update_generator.rb +27 -0
  73. data/lib/generators/workflowable/WORKFLOW_README +27 -0
  74. data/lib/generators/workflowable/templates/workflow.rb.tt +72 -0
  75. data/lib/generators/workflowable/templates/workflow_test.rb.tt +62 -0
  76. data/lib/generators/workflowable/workflow_generator.rb +60 -0
  77. data/lib/tasks/better_service_tasks.rake +4 -0
  78. metadata +180 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Runtime
6
+ # Raised when user is not authorized to perform the action
7
+ #
8
+ # This error is raised when the authorize_with block returns false.
9
+ # Authorization checks happen before the service execution begins.
10
+ #
11
+ # @example Authorization failure
12
+ # class Post::DestroyService < BetterService::Services::DestroyService
13
+ # model_class Post
14
+ #
15
+ # schema do
16
+ # required(:id).filled(:integer)
17
+ # end
18
+ #
19
+ # authorize_with do
20
+ # resource.user_id == user.id # Only owner can delete
21
+ # end
22
+ # end
23
+ #
24
+ # # User tries to delete someone else's post
25
+ # Post::DestroyService.new(current_user, params: { id: other_users_post_id }).call
26
+ # # => raises AuthorizationError
27
+ #
28
+ # @example Handling authorization errors
29
+ # begin
30
+ # MyService.new(user, params: params).call
31
+ # rescue BetterService::Errors::Runtime::AuthorizationError => e
32
+ # render json: { error: e.message }, status: :forbidden
33
+ # end
34
+ class AuthorizationError < RuntimeError
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Runtime
6
+ # Raised when a database operation fails
7
+ #
8
+ # This error wraps ActiveRecord database errors such as RecordInvalid,
9
+ # RecordNotSaved, constraint violations, and other database-level failures.
10
+ #
11
+ # @example Record validation fails
12
+ # class UserCreateService < BetterService::Services::CreateService
13
+ # model_class User
14
+ #
15
+ # schema do
16
+ # required(:email).filled(:string)
17
+ # end
18
+ # end
19
+ #
20
+ # UserCreateService.new(user, params: { email: "invalid" }).call
21
+ # # => raises DatabaseError wrapping ActiveRecord::RecordInvalid
22
+ #
23
+ # @example Constraint violation
24
+ # class MyService < BetterService::Services::Base
25
+ # schema { required(:user_id).filled(:integer) }
26
+ #
27
+ # process_with do |data|
28
+ # User.create!(email: "duplicate@example.com") # Unique constraint fails
29
+ # end
30
+ # end
31
+ #
32
+ # MyService.new(user, params: { user_id: 1 }).call
33
+ # # => raises DatabaseError
34
+ class DatabaseError < RuntimeError
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Runtime
6
+ # Raised when unexpected error occurs during service execution
7
+ #
8
+ # This error wraps unexpected StandardError exceptions that occur during
9
+ # the service's search, process, transform, or respond phases.
10
+ #
11
+ # @example Unexpected error in service
12
+ # class MyService < BetterService::Services::Base
13
+ # schema { }
14
+ #
15
+ # process_with do |data|
16
+ # # Some operation that fails unexpectedly
17
+ # third_party_api.call # raises SocketError
18
+ # end
19
+ # end
20
+ #
21
+ # MyService.new(user, params: {}).call
22
+ # # => raises ExecutionError wrapping SocketError
23
+ class ExecutionError < RuntimeError
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Runtime
6
+ # Raised when a required resource is not found
7
+ #
8
+ # This error wraps ActiveRecord::RecordNotFound exceptions raised during
9
+ # service execution (usually in the search phase).
10
+ #
11
+ # @example Resource not found
12
+ # class UserShowService < BetterService::Services::ShowService
13
+ # model_class User
14
+ #
15
+ # schema do
16
+ # required(:id).filled(:integer)
17
+ # end
18
+ # end
19
+ #
20
+ # UserShowService.new(user, params: { id: 99999 }).call
21
+ # # => raises ResourceNotFoundError wrapping ActiveRecord::RecordNotFound
22
+ #
23
+ # @example In custom service
24
+ # class MyService < BetterService::Services::Base
25
+ # schema { required(:user_id).filled(:integer) }
26
+ #
27
+ # search_with do
28
+ # User.find(params[:user_id]) # Raises RecordNotFound if not exists
29
+ # end
30
+ # end
31
+ #
32
+ # MyService.new(user, params: { user_id: 99999 }).call
33
+ # # => raises ResourceNotFoundError
34
+ class ResourceNotFoundError < RuntimeError
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Runtime
6
+ # Base class for all runtime errors in BetterService
7
+ #
8
+ # Runtime errors are raised when something goes wrong during service execution
9
+ # due to external factors (database, network, invalid data, etc.).
10
+ # These are not programming errors.
11
+ #
12
+ # @example
13
+ # raise BetterService::Errors::Runtime::RuntimeError.new(
14
+ # "Runtime error occurred",
15
+ # code: :runtime_error,
16
+ # context: { service: "MyService", operation: "fetch_data" }
17
+ # )
18
+ class RuntimeError < BetterServiceError
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Runtime
6
+ # Raised when a database transaction fails
7
+ #
8
+ # This error is raised when ActiveRecord transaction operations fail,
9
+ # such as deadlocks, serialization errors, or constraint violations.
10
+ #
11
+ # @example Transaction failure
12
+ # class MyService < BetterService::Services::Base
13
+ # config do
14
+ # use_transaction true
15
+ # end
16
+ #
17
+ # schema { }
18
+ #
19
+ # process_with do |data|
20
+ # # Database operation that causes deadlock
21
+ # User.transaction do
22
+ # user.lock!
23
+ # other_user.lock! # Deadlock!
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # MyService.new(user, params: {}).call
29
+ # # => raises TransactionError
30
+ class TransactionError < RuntimeError
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Runtime
6
+ # Raised when parameter validation fails
7
+ #
8
+ # This error is raised during service initialization when Dry::Schema
9
+ # validation fails. The validation errors are available in the context.
10
+ #
11
+ # @example Validation failure
12
+ # class MyService < BetterService::Services::Base
13
+ # schema do
14
+ # required(:email).filled(:string)
15
+ # required(:age).filled(:integer, gt?: 18)
16
+ # end
17
+ # end
18
+ #
19
+ # MyService.new(user, params: { email: "", age: 15 }).call
20
+ # # => raises ValidationError with context:
21
+ # # {
22
+ # # service: "MyService",
23
+ # # validation_errors: {
24
+ # # email: ["must be filled"],
25
+ # # age: ["must be greater than 18"]
26
+ # # }
27
+ # # }
28
+ #
29
+ # @example Handling validation errors
30
+ # begin
31
+ # MyService.new(user, params: invalid_params).call
32
+ # rescue BetterService::Errors::Runtime::ValidationError => e
33
+ # render json: {
34
+ # error: e.message,
35
+ # validation_errors: e.context[:validation_errors]
36
+ # }, status: :unprocessable_entity
37
+ # end
38
+ class ValidationError < RuntimeError
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Workflowable
6
+ module Configuration
7
+ # Raised when a workflow has duplicate step names
8
+ #
9
+ # Each step in a workflow must have a unique name. This error is raised
10
+ # if you try to define multiple steps with the same name.
11
+ #
12
+ # @example Duplicate step names
13
+ # class MyWorkflow < BetterService::Workflow
14
+ # step :create_user,
15
+ # with: User::CreateService
16
+ #
17
+ # step :create_user, # Duplicate name!
18
+ # with: Profile::CreateService
19
+ # end
20
+ #
21
+ # # => raises DuplicateStepError during class definition
22
+ class DuplicateStepError < WorkflowConfigurationError
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Workflowable
6
+ module Configuration
7
+ class InvalidStepError < WorkflowConfigurationError
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Workflowable
6
+ module Configuration
7
+ # Raised when a referenced workflow step is not found
8
+ #
9
+ # This error is raised when trying to access a step that doesn't exist
10
+ # in the workflow definition, such as in dependencies or conditionals.
11
+ #
12
+ # @example Step not found
13
+ # class MyWorkflow < BetterService::Workflow
14
+ # step :first_step,
15
+ # with: FirstService
16
+ #
17
+ # step :second_step,
18
+ # with: SecondService,
19
+ # if: ->(ctx) { ctx.non_existent_step.success? } # Step doesn't exist
20
+ # end
21
+ #
22
+ # MyWorkflow.new(user, params: {}).call
23
+ # # => raises StepNotFoundError
24
+ class StepNotFoundError < WorkflowConfigurationError
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Workflowable
6
+ module Configuration
7
+ # Base class for workflow configuration errors
8
+ #
9
+ # Raised when a workflow is incorrectly configured, such as invalid steps,
10
+ # missing service classes, or conflicting configurations.
11
+ #
12
+ # @example Invalid workflow configuration
13
+ # class MyWorkflow < BetterService::Workflow
14
+ # step :invalid,
15
+ # with: nil # Missing service class
16
+ # end
17
+ #
18
+ # # => raises WorkflowConfigurationError during class definition
19
+ class WorkflowConfigurationError < BetterService::Errors::Configuration::ConfigurationError
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Workflowable
6
+ module Runtime
7
+ # Raised when workflow rollback fails
8
+ #
9
+ # This error is raised when a step's rollback block fails during workflow rollback.
10
+ # Rollback failures are serious as they may leave the system in an inconsistent state.
11
+ #
12
+ # @example Rollback failure
13
+ # class MyWorkflow < BetterService::Workflow
14
+ # step :create_user,
15
+ # with: User::CreateService,
16
+ # rollback: ->(ctx) { ctx.user.destroy! }
17
+ #
18
+ # step :charge_payment,
19
+ # with: Payment::ChargeService,
20
+ # rollback: ->(ctx) {
21
+ # # Rollback fails - payment gateway is down
22
+ # PaymentGateway.refund(ctx.charge.id) # raises error
23
+ # }
24
+ #
25
+ # step :send_email,
26
+ # with: Email::WelcomeService # This step fails
27
+ # end
28
+ #
29
+ # MyWorkflow.new(user, params: {}).call
30
+ # # => send_email fails, triggers rollback
31
+ # # => charge_payment rollback fails
32
+ # # => raises RollbackError with context:
33
+ # # {
34
+ # # workflow: "MyWorkflow",
35
+ # # step: :charge_payment,
36
+ # # executed_steps: [:create_user, :charge_payment]
37
+ # # }
38
+ #
39
+ # @note Rollback errors indicate potential data inconsistency and should be
40
+ # monitored and handled carefully in production systems.
41
+ class RollbackError < WorkflowRuntimeError
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Workflowable
6
+ module Runtime
7
+ # Raised when a workflow step execution fails
8
+ #
9
+ # This error is raised when a step in the workflow fails and the step is not optional.
10
+ # The error includes context about which step failed and what steps were executed.
11
+ #
12
+ # @example Step execution failure
13
+ # class MyWorkflow < BetterService::Workflow
14
+ # step :create_user,
15
+ # with: User::CreateService
16
+ #
17
+ # step :charge_payment,
18
+ # with: Payment::ChargeService # This step fails
19
+ #
20
+ # step :send_email,
21
+ # with: Email::WelcomeService # This step never executes
22
+ # end
23
+ #
24
+ # MyWorkflow.new(user, params: {}).call
25
+ # # => raises StepExecutionError with context:
26
+ # # {
27
+ # # workflow: "MyWorkflow",
28
+ # # step: :charge_payment,
29
+ # # steps_executed: [:create_user],
30
+ # # errors: { ... }
31
+ # # }
32
+ #
33
+ # @example Optional step failures don't raise
34
+ # class MyWorkflow < BetterService::Workflow
35
+ # step :create_user,
36
+ # with: User::CreateService
37
+ #
38
+ # step :send_email,
39
+ # with: Email::WelcomeService,
40
+ # optional: true # Failure won't raise StepExecutionError
41
+ # end
42
+ class StepExecutionError < WorkflowRuntimeError
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Workflowable
6
+ module Runtime
7
+ # Raised when workflow execution fails
8
+ #
9
+ # This error is raised when unexpected errors occur during workflow execution,
10
+ # wrapping the original exception with workflow context.
11
+ #
12
+ # @example Workflow execution failure
13
+ # class MyWorkflow < BetterService::Workflow
14
+ # step :create_user,
15
+ # with: User::CreateService
16
+ #
17
+ # step :send_email,
18
+ # with: Email::WelcomeService
19
+ # end
20
+ #
21
+ # MyWorkflow.new(user, params: {}).call
22
+ # # If unexpected error occurs => raises WorkflowExecutionError
23
+ #
24
+ # @example Error context
25
+ # begin
26
+ # MyWorkflow.new(user, params: params).call
27
+ # rescue BetterService::Errors::Workflowable::Runtime::WorkflowExecutionError => e
28
+ # e.context
29
+ # # => {
30
+ # # workflow: "MyWorkflow",
31
+ # # steps_executed: [:create_user],
32
+ # # steps_skipped: []
33
+ # # }
34
+ # end
35
+ class WorkflowExecutionError < WorkflowRuntimeError
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Errors
5
+ module Workflowable
6
+ module Runtime
7
+ # Base class for workflow runtime errors
8
+ #
9
+ # Raised when errors occur during workflow execution, such as step failures,
10
+ # rollback failures, or workflow execution errors.
11
+ #
12
+ # @example Workflow runtime error
13
+ # class MyWorkflow < BetterService::Workflow
14
+ # step :first_step,
15
+ # with: FirstService
16
+ # end
17
+ #
18
+ # MyWorkflow.new(user, params: {}).call
19
+ # # If FirstService fails => raises WorkflowRuntimeError (or subclass)
20
+ class WorkflowRuntimeError < BetterService::Errors::Runtime::RuntimeError
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ if defined?(Rails)
2
+ module BetterService
3
+ class Railtie < ::Rails::Railtie
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module BetterService
6
+ module Services
7
+ # ActionService - Specialized service for custom actions/transitions
8
+ #
9
+ # Returns: { resource: {}, metadata: { action: :custom_action_name } }
10
+ #
11
+ # Example:
12
+ # class Bookings::AcceptService < BetterService::Services::ActionService
13
+ # action_name :accepted # Sets metadata action
14
+ #
15
+ # schema do
16
+ # required(:id).filled(:integer)
17
+ # end
18
+ #
19
+ # search_with do
20
+ # { resource: user.bookings.find(params[:id]) }
21
+ # end
22
+ #
23
+ # process_with do |data|
24
+ # booking = data[:resource]
25
+ # booking.update!(status: 'accepted', accepted_at: Time.current)
26
+ # { resource: booking }
27
+ # end
28
+ # end
29
+ class ActionService < Services::Base
30
+ # Default action_name to nil - subclasses MUST set it
31
+ self._action_name = nil
32
+
33
+ def self.action_name(name)
34
+ self._action_name = name.to_sym
35
+ end
36
+
37
+ # Default schema - requires id parameter for actions
38
+ schema do
39
+ required(:id).filled
40
+ end
41
+
42
+ private
43
+
44
+ # Override respond to ensure resource key is present
45
+ def respond(data)
46
+ # Get base result (from custom respond_with block or default)
47
+ if self.class._respond_block
48
+ result = instance_exec(data, &self.class._respond_block)
49
+ else
50
+ result = success_result("Action completed successfully", data)
51
+ end
52
+
53
+ # Ensure resource key exists (default to nil if not provided)
54
+ result[:resource] ||= nil
55
+
56
+ result
57
+ end
58
+ end
59
+ end
60
+ end