better_controller 0.1.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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +95 -0
  5. data/CHANGELOG.md +18 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +624 -0
  9. data/Rakefile +12 -0
  10. data/examples/api_controller.rb +31 -0
  11. data/examples/application_controller.rb +51 -0
  12. data/examples/products_controller.rb +25 -0
  13. data/examples/user_serializer.rb +15 -0
  14. data/examples/user_service.rb +31 -0
  15. data/examples/users_controller.rb +86 -0
  16. data/lib/better_controller/action_helpers.rb +76 -0
  17. data/lib/better_controller/base.rb +61 -0
  18. data/lib/better_controller/configuration.rb +69 -0
  19. data/lib/better_controller/logging.rb +101 -0
  20. data/lib/better_controller/method_not_overridden_error.rb +13 -0
  21. data/lib/better_controller/pagination.rb +44 -0
  22. data/lib/better_controller/parameter_validation.rb +86 -0
  23. data/lib/better_controller/params_helpers.rb +109 -0
  24. data/lib/better_controller/railtie.rb +18 -0
  25. data/lib/better_controller/resources_controller.rb +263 -0
  26. data/lib/better_controller/response_helpers.rb +74 -0
  27. data/lib/better_controller/serializer.rb +85 -0
  28. data/lib/better_controller/service.rb +94 -0
  29. data/lib/better_controller/version.rb +5 -0
  30. data/lib/better_controller.rb +55 -0
  31. data/lib/generators/better_controller/controller_generator.rb +65 -0
  32. data/lib/generators/better_controller/install_generator.rb +22 -0
  33. data/lib/generators/better_controller/templates/README +87 -0
  34. data/lib/generators/better_controller/templates/controller.rb +78 -0
  35. data/lib/generators/better_controller/templates/initializer.rb +31 -0
  36. data/lib/generators/better_controller/templates/serializer.rb +32 -0
  37. data/lib/generators/better_controller/templates/service.rb +57 -0
  38. data/lib/tasks/better_controller_tasks.rake +61 -0
  39. data/sig/better_controller.rbs +4 -0
  40. metadata +226 -0
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example of a controller using BetterController's standard_actions
4
+ class ProductsController < ApplicationController
5
+ include BetterController
6
+
7
+ # Use the standard_actions helper to generate CRUD actions
8
+ standard_actions Product, paginate: true
9
+
10
+ private
11
+
12
+ # Strong parameters for Product
13
+ def product_params
14
+ params.require(:product).permit(:name, :description, :price, :category_id)
15
+ end
16
+
17
+ # You can still override any of the standard actions if needed
18
+ # For example:
19
+ # def index
20
+ # execute_action do
21
+ # products = Product.where(category_id: params[:category_id])
22
+ # respond_with_pagination(products)
23
+ # end
24
+ # end
25
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example of a serializer for User resources
4
+ class UserSerializer
5
+ include BetterController::Serializer
6
+
7
+ # Define the attributes to include in the serialized output
8
+ attributes :id, :name, :email, :role, :created_at, :updated_at
9
+
10
+ # Define methods to include in the serialized output
11
+ methods :full_name, :active_status
12
+
13
+ # Define associations to include in the serialized output
14
+ # associations posts: PostSerializer, comments: CommentSerializer
15
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example of a service class for User resources
4
+ class UserService < BetterController::Service
5
+ protected
6
+
7
+ # Define the resource class for this service
8
+ def resource_class
9
+ User
10
+ end
11
+
12
+ # Customize the resource scope if needed
13
+ def resource_scope(ancestry_params = {})
14
+ scope = super
15
+
16
+ # Example of adding custom scoping
17
+ scope = scope.active if ancestry_params[:active_only]
18
+
19
+ scope
20
+ end
21
+
22
+ # Customize attribute preparation if needed
23
+ def prepare_attributes(attributes, ancestry_params = {})
24
+ prepared = super
25
+
26
+ # Example of adding custom attribute preparation
27
+ prepared[:created_by] = ancestry_params[:current_user_id] if ancestry_params[:current_user_id]
28
+
29
+ prepared
30
+ end
31
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'user_service'
4
+ require_relative 'user_serializer'
5
+
6
+ # Example of a Rails controller using BetterController::ResourcesController
7
+ class UsersController < ApplicationController
8
+ include BetterController::ResourcesController
9
+
10
+ # Define required parameters for actions
11
+ requires_params :create, :name, :email
12
+ requires_params :update, :id
13
+
14
+ # Define parameter schema for validation
15
+ param_schema :create, {
16
+ name: { required: true, type: String },
17
+ email: { required: true, format: /\A[^@\s]+@[^@\s]+\z/ },
18
+ role: { in: %w[admin user guest] },
19
+ }
20
+
21
+ # You can override any of the ResourcesController methods if needed
22
+ # For example, to customize the index action:
23
+ def index
24
+ execute_action do
25
+ @resource_collection = resource_collection_resolver
26
+
27
+ # Add pagination metadata
28
+ if @resource_collection.respond_to?(:page)
29
+ paginated = @resource_collection.page(params[:page] || 1).per(params[:per_page] || 25)
30
+ add_meta(:pagination, {
31
+ current_page: paginated.current_page,
32
+ total_pages: paginated.total_pages,
33
+ total_count: paginated.total_count,
34
+ })
35
+ @resource_collection = paginated
36
+ end
37
+
38
+ respond_with_success(@resource_collection, options: { meta: meta })
39
+ end
40
+ end
41
+
42
+ # You can also add custom actions
43
+ def activate
44
+ execute_action do
45
+ @resource = resource_finder
46
+ @resource.update(active: true)
47
+
48
+ respond_with_success(@resource, options: {
49
+ message: "User #{@resource.name} has been activated",
50
+ })
51
+ end
52
+ end
53
+
54
+ protected
55
+
56
+ # Define the resource service class
57
+ def resource_service_class
58
+ UserService
59
+ end
60
+
61
+ # Define the root key for resource parameters
62
+ def resource_params_root_key
63
+ :user
64
+ end
65
+
66
+ # Customize the ancestry parameters if needed
67
+ def ancestry_key_params
68
+ # Example: passing the current user ID to the service
69
+ { current_user_id: current_user&.id }
70
+ end
71
+
72
+ # Customize create message
73
+ def create_message
74
+ "User #{@resource.name} has been created successfully"
75
+ end
76
+
77
+ # Customize update message
78
+ def update_message
79
+ "User #{@resource.name} has been updated successfully"
80
+ end
81
+
82
+ # Customize destroy message
83
+ def destroy_message
84
+ 'User has been deleted successfully'
85
+ end
86
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterController
4
+ # Module providing helper methods for common controller actions
5
+ module ActionHelpers
6
+ extend ActiveSupport::Concern
7
+
8
+ # Define standard CRUD actions for a resource
9
+ # @param resource_class [Class] The resource class (e.g., User)
10
+ # @param options [Hash] Options for customizing the actions
11
+ module ClassMethods
12
+ def standard_actions(resource_class, options = {})
13
+ resource_name = resource_class.name.underscore
14
+ resource_name.pluralize
15
+
16
+ # Define index action
17
+ define_method(:index) do
18
+ execute_action do
19
+ collection = resource_class.all
20
+
21
+ # Apply scopes if provided
22
+ collection = apply_scopes(collection) if respond_to?(:apply_scopes)
23
+
24
+ # Apply pagination if requested
25
+ if options[:paginate]
26
+ respond_with_pagination(collection, page: params[:page], per_page: params[:per_page])
27
+ else
28
+ respond_with_success(collection)
29
+ end
30
+ end
31
+ end
32
+
33
+ # Define show action
34
+ define_method(:show) do
35
+ execute_action do
36
+ resource = resource_class.find(params[:id])
37
+ respond_with_success(resource)
38
+ end
39
+ end
40
+
41
+ # Define create action
42
+ define_method(:create) do
43
+ execute_action do
44
+ resource = resource_class.new(send(:"#{resource_name}_params"))
45
+
46
+ with_transaction do
47
+ resource.save!
48
+ respond_with_success(resource, status: :created)
49
+ end
50
+ end
51
+ end
52
+
53
+ # Define update action
54
+ define_method(:update) do
55
+ execute_action do
56
+ resource = resource_class.find(params[:id])
57
+
58
+ with_transaction do
59
+ resource.update!(send(:"#{resource_name}_params"))
60
+ respond_with_success(resource)
61
+ end
62
+ end
63
+ end
64
+
65
+ # Define destroy action
66
+ define_method(:destroy) do
67
+ execute_action do
68
+ resource = resource_class.find(params[:id])
69
+ resource.destroy
70
+ respond_with_success(nil, status: :no_content)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterController
4
+ # Base module that provides core functionality for enhanced controllers
5
+ module Base
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # Class methods and configurations to be included in the controller
10
+ class_attribute :better_controller_options, default: {}
11
+
12
+ # Set up rescue handlers if enabled
13
+ if defined?(rescue_from) && BetterController.config.error_handling[:detailed_errors]
14
+ rescue_from StandardError, with: :better_controller_handle_error
15
+ end
16
+ end
17
+
18
+ # Instance methods for controllers
19
+
20
+ # Execute an action with enhanced error handling and response formatting
21
+ # @param action_block [Proc] The block containing the action logic
22
+ # @return [Object] The formatted response
23
+ def execute_action(&)
24
+ log_debug("Executing action: #{action_name}") if respond_to?(:action_name)
25
+ result = instance_eval(&)
26
+ respond_with_success(result)
27
+ rescue StandardError => e
28
+ handle_exception(e)
29
+ end
30
+
31
+ # Helper method to simplify common controller patterns
32
+ # @param options [Hash] Options for the action
33
+ # @return [Object] The result of the action
34
+ def with_transaction(options = {}, &)
35
+ ActiveRecord::Base.transaction(&)
36
+ rescue StandardError => e
37
+ handle_exception(e, options)
38
+ end
39
+
40
+ # Handle exceptions raised by the controller
41
+ # @param exception [Exception] The exception to handle
42
+ def better_controller_handle_error(exception)
43
+ handle_exception(exception)
44
+ end
45
+
46
+ private
47
+
48
+ # Handle exceptions in a standardized way
49
+ # @param exception [Exception] The exception to handle
50
+ # @param options [Hash] Options for handling the exception
51
+ # @return [Object] The error response
52
+ def handle_exception(exception, options = {})
53
+ # Log the exception if logging is enabled
54
+ if respond_to?(:log_exception) && BetterController.config.error_handling[:log_errors]
55
+ log_exception(exception, { controller: self.class.name, action: action_name })
56
+ end
57
+
58
+ respond_with_error(exception, options)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterController
4
+ # Configuration module for BetterController
5
+ module Configuration
6
+ # Default configuration options
7
+ DEFAULTS = {
8
+ pagination: {
9
+ enabled: true,
10
+ per_page: 25,
11
+ },
12
+ serialization: {
13
+ include_root: false,
14
+ camelize_keys: true,
15
+ },
16
+ error_handling: {
17
+ log_errors: true,
18
+ detailed_errors: true,
19
+ },
20
+ }.freeze
21
+
22
+ class << self
23
+ # Get the current configuration
24
+ # @return [Hash] The current configuration
25
+ def options
26
+ @options ||= DEFAULTS.deep_dup
27
+ end
28
+
29
+ # Configure BetterController
30
+ # @yield [config] The configuration block
31
+ def configure
32
+ yield(options) if block_given?
33
+ end
34
+
35
+ # Reset the configuration to defaults
36
+ def reset!
37
+ @options = DEFAULTS.deep_dup
38
+ end
39
+
40
+ # Get a specific configuration option
41
+ # @param key [Symbol] The configuration key
42
+ # @return [Object] The configuration value
43
+ delegate :[], to: :options
44
+
45
+ # Set a specific configuration option
46
+ # @param key [Symbol] The configuration key
47
+ # @param value [Object] The configuration value
48
+ delegate :[]=, to: :options
49
+ end
50
+
51
+ # Get the pagination configuration
52
+ # @return [Hash] The pagination configuration
53
+ def self.pagination
54
+ options[:pagination]
55
+ end
56
+
57
+ # Get the serialization configuration
58
+ # @return [Hash] The serialization configuration
59
+ def self.serialization
60
+ options[:serialization]
61
+ end
62
+
63
+ # Get the error handling configuration
64
+ # @return [Hash] The error handling configuration
65
+ def self.error_handling
66
+ options[:error_handling]
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterController
4
+ # Module providing logging capabilities
5
+ module Logging
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # Set up a logger for the controller
10
+ class_attribute :better_controller_logger, default: Rails.logger if defined?(Rails)
11
+ end
12
+
13
+ # Log a message at the info level
14
+ # @param message [String] The message to log
15
+ # @param tags [Hash] Additional tags for the log
16
+ def log_info(message, tags = {})
17
+ log(:info, message, tags)
18
+ end
19
+
20
+ # Log a message at the debug level
21
+ # @param message [String] The message to log
22
+ # @param tags [Hash] Additional tags for the log
23
+ def log_debug(message, tags = {})
24
+ log(:debug, message, tags)
25
+ end
26
+
27
+ # Log a message at the warn level
28
+ # @param message [String] The message to log
29
+ # @param tags [Hash] Additional tags for the log
30
+ def log_warn(message, tags = {})
31
+ log(:warn, message, tags)
32
+ end
33
+
34
+ # Log a message at the error level
35
+ # @param message [String] The message to log
36
+ # @param tags [Hash] Additional tags for the log
37
+ def log_error(message, tags = {})
38
+ log(:error, message, tags)
39
+ end
40
+
41
+ # Log a message at the fatal level
42
+ # @param message [String] The message to log
43
+ # @param tags [Hash] Additional tags for the log
44
+ def log_fatal(message, tags = {})
45
+ log(:fatal, message, tags)
46
+ end
47
+
48
+ # Log an exception
49
+ # @param exception [Exception] The exception to log
50
+ # @param tags [Hash] Additional tags for the log
51
+ def log_exception(exception, tags = {})
52
+ return unless BetterController.config.error_handling[:log_errors]
53
+
54
+ tags = tags.merge(
55
+ exception_class: exception.class.name,
56
+ backtrace: exception.backtrace&.join("\n")
57
+ )
58
+
59
+ log_error(exception.message, tags)
60
+ end
61
+
62
+ private
63
+
64
+ # Log a message at the specified level
65
+ # @param level [Symbol] The log level
66
+ # @param message [String] The message to log
67
+ # @param tags [Hash] Additional tags for the log
68
+ def log(level, message, tags = {})
69
+ return unless logger
70
+
71
+ tags = tags.merge(controller: self.class.name, action: action_name) if respond_to?(:action_name)
72
+
73
+ if tags.empty?
74
+ logger.send(level, message)
75
+ else
76
+ logger.send(level) { "[BetterController] #{message} #{tags.inspect}" }
77
+ end
78
+ end
79
+
80
+ # Get the logger
81
+ # @return [Logger] The logger
82
+ def logger
83
+ self.class.better_controller_logger
84
+ end
85
+
86
+ # Module providing class methods for logging
87
+ module ClassMethods
88
+ # Set the logger for the controller
89
+ # @param logger [Logger] The logger to use
90
+ def logger=(logger)
91
+ self.better_controller_logger = logger
92
+ end
93
+
94
+ # Get the logger for the controller
95
+ # @return [Logger] The logger
96
+ def logger
97
+ better_controller_logger
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterController
4
+ # Error raised when a required method is not overridden
5
+ class MethodNotOverriddenError < StandardError
6
+ # Initialize a new error
7
+ # @param method_name [Symbol, String] The method that should be overridden
8
+ # @param instance [Object] The instance where the method should be overridden
9
+ def initialize(method_name, instance)
10
+ super("Method '#{method_name}' must be overridden in #{instance.class.name}")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterController
4
+ # Module providing pagination functionality for ActiveRecord collections
5
+ module Pagination
6
+ extend ActiveSupport::Concern
7
+
8
+ # Paginate a collection
9
+ # @param collection [ActiveRecord::Relation] The collection to paginate
10
+ # @param page [Integer] The page number
11
+ # @param per_page [Integer] The number of items per page
12
+ # @return [ActiveRecord::Relation] The paginated collection
13
+ def paginate(collection, page: nil, per_page: nil)
14
+ page = (page || params[:page] || 1).to_i
15
+ per_page = (per_page || params[:per_page] || 25).to_i
16
+
17
+ paginated = collection.page(page).per(per_page)
18
+
19
+ # Add pagination metadata
20
+ if respond_to?(:add_meta)
21
+ add_meta(:pagination, {
22
+ current_page: paginated.current_page,
23
+ total_pages: paginated.total_pages,
24
+ total_count: paginated.total_count,
25
+ per_page: per_page,
26
+ })
27
+ end
28
+
29
+ paginated
30
+ end
31
+
32
+ # Module providing class methods for pagination
33
+ module ClassMethods
34
+ # Configure pagination defaults
35
+ # @param options [Hash] Pagination options
36
+ def configure_pagination(options = {})
37
+ class_attribute :pagination_options, default: {
38
+ enabled: true,
39
+ per_page: 25,
40
+ }.merge(options)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterController
4
+ # Module providing parameter validation helpers for controllers
5
+ module ParameterValidation
6
+ extend ActiveSupport::Concern
7
+
8
+ # Class methods for parameter validation
9
+ module ClassMethods
10
+ # Define required parameters for an action
11
+ # @param action_name [Symbol] The name of the action
12
+ # @param params [Array<Symbol>] The required parameters
13
+ def requires_params(action_name, *params)
14
+ before_action only: action_name do
15
+ validate_required_params(*params)
16
+ end
17
+ end
18
+
19
+ # Define parameter schema for an action
20
+ # @param action_name [Symbol] The name of the action
21
+ # @param schema [Hash] The parameter schema
22
+ def param_schema(action_name, schema)
23
+ before_action only: action_name do
24
+ validate_param_schema(schema)
25
+ end
26
+ end
27
+ end
28
+
29
+ # Instance methods for parameter validation
30
+
31
+ # Validate that required parameters are present
32
+ # @param params [Array<Symbol>] The required parameters
33
+ # @raise [BetterController::Error] If a required parameter is missing
34
+ def validate_required_params(*params)
35
+ missing_params = []
36
+
37
+ params.each do |param|
38
+ missing_params << param unless parameter_present?(param)
39
+ end
40
+
41
+ unless missing_params.empty?
42
+ error_message = "Missing required parameters: #{missing_params.join(', ')}"
43
+ raise BetterController::Error, error_message
44
+ end
45
+
46
+ true
47
+ end
48
+
49
+ # Validate parameters against a schema
50
+ # @param schema [Hash] The parameter schema
51
+ # @raise [BetterController::Error] If parameters don't match the schema
52
+ def validate_param_schema(schema)
53
+ errors = []
54
+
55
+ schema.each do |param, rules|
56
+ value = params[param]
57
+
58
+ if rules[:required] && value.nil?
59
+ errors << "#{param} is required"
60
+ next
61
+ end
62
+
63
+ next if value.nil?
64
+
65
+ errors << "#{param} must be a #{rules[:type]}" if rules[:type] && !value.is_a?(rules[:type])
66
+
67
+ errors << "#{param} must be one of: #{rules[:in].join(', ')}" if rules[:in]&.exclude?(value)
68
+
69
+ errors << "#{param} has invalid format" if rules[:format] && rules[:format] !~ value.to_s
70
+ end
71
+
72
+ raise BetterController::Error, errors.join(', ') unless errors.empty?
73
+
74
+ true
75
+ end
76
+
77
+ private
78
+
79
+ # Check if a parameter is present
80
+ # @param param [Symbol] The parameter to check
81
+ # @return [Boolean] Whether the parameter is present
82
+ def parameter_present?(param)
83
+ params.key?(param.to_s) || params.key?(param.to_sym)
84
+ end
85
+ end
86
+ end