better_service 2.0.0 → 2.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +2 -0
  3. data/README.md +98 -45
  4. data/Rakefile +7 -209
  5. data/config/locales/better_service.en.yml +15 -0
  6. data/lib/better_service/cache_service.rb +4 -4
  7. data/lib/better_service/concerns/instrumentation.rb +59 -14
  8. data/lib/better_service/concerns/serviceable/authorizable.rb +1 -1
  9. data/lib/better_service/concerns/serviceable/messageable.rb +70 -1
  10. data/lib/better_service/concerns/serviceable/repository_aware.rb +8 -3
  11. data/lib/better_service/concerns/workflowable/callbacks.rb +27 -27
  12. data/lib/better_service/concerns/workflowable/step.rb +39 -5
  13. data/lib/better_service/errors/better_service_error.rb +4 -0
  14. data/lib/better_service/errors/runtime/authorization_error.rb +4 -1
  15. data/lib/better_service/errors/runtime/database_error.rb +4 -1
  16. data/lib/better_service/errors/runtime/execution_error.rb +4 -1
  17. data/lib/better_service/errors/runtime/invalid_result_error.rb +28 -0
  18. data/lib/better_service/errors/runtime/resource_not_found_error.rb +4 -1
  19. data/lib/better_service/errors/runtime/validation_error.rb +4 -1
  20. data/lib/better_service/repository/base_repository.rb +1 -1
  21. data/lib/better_service/result.rb +110 -0
  22. data/lib/better_service/services/base.rb +216 -57
  23. data/lib/better_service/version.rb +1 -1
  24. data/lib/better_service/workflows/branch_group.rb +1 -1
  25. data/lib/better_service.rb +1 -6
  26. data/lib/generators/serviceable/action_generator.rb +11 -0
  27. data/lib/generators/serviceable/base_generator.rb +109 -0
  28. data/lib/generators/serviceable/create_generator.rb +11 -0
  29. data/lib/generators/serviceable/destroy_generator.rb +11 -0
  30. data/lib/generators/serviceable/index_generator.rb +11 -0
  31. data/lib/generators/serviceable/scaffold_generator.rb +29 -7
  32. data/lib/generators/serviceable/show_generator.rb +11 -0
  33. data/lib/generators/serviceable/templates/action_service.rb.tt +8 -3
  34. data/lib/generators/serviceable/templates/base_locale.en.yml.tt +53 -0
  35. data/lib/generators/serviceable/templates/base_service.rb.tt +78 -0
  36. data/lib/generators/serviceable/templates/base_service_test.rb.tt +64 -0
  37. data/lib/generators/serviceable/templates/create_service.rb.tt +29 -18
  38. data/lib/generators/serviceable/templates/destroy_service.rb.tt +16 -29
  39. data/lib/generators/serviceable/templates/index_service.rb.tt +16 -34
  40. data/lib/generators/serviceable/templates/repository.rb.tt +76 -0
  41. data/lib/generators/serviceable/templates/repository_test.rb.tt +124 -0
  42. data/lib/generators/serviceable/templates/show_service.rb.tt +10 -38
  43. data/lib/generators/serviceable/templates/update_service.rb.tt +24 -38
  44. data/lib/generators/serviceable/update_generator.rb +11 -0
  45. metadata +13 -12
  46. data/lib/better_service/concerns/serviceable/viewable.rb +0 -33
  47. data/lib/better_service/services/action_service.rb +0 -60
  48. data/lib/better_service/services/create_service.rb +0 -63
  49. data/lib/better_service/services/destroy_service.rb +0 -60
  50. data/lib/better_service/services/index_service.rb +0 -56
  51. data/lib/better_service/services/show_service.rb +0 -44
  52. data/lib/better_service/services/update_service.rb +0 -61
@@ -9,6 +9,9 @@ module Serviceable
9
9
 
10
10
  desc "Generate a Show service for displaying a single resource"
11
11
 
12
+ class_option :base_class, type: :string, default: nil,
13
+ desc: "Custom base class to inherit from (e.g., Articles::BaseService)"
14
+
12
15
  def create_service_file
13
16
  template "show_service.rb.tt", File.join("app/services", class_path, "#{file_name}/show_service.rb")
14
17
  end
@@ -22,6 +25,14 @@ module Serviceable
22
25
  def service_class_name
23
26
  "#{class_name}::ShowService"
24
27
  end
28
+
29
+ def parent_class
30
+ options[:base_class] || "BetterService::Services::Base"
31
+ end
32
+
33
+ def using_base_service?
34
+ options[:base_class].present?
35
+ end
25
36
  end
26
37
  end
27
38
  end
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  <% module_namespacing do -%>
4
- class <%= class_name %>::<%= action_name.camelize %>Service < BetterService::Services::ActionService
5
- action_name :<%= action_name %>
4
+ class <%= class_name %>::<%= action_name.camelize %>Service < <%= parent_class %>
5
+ # Action name for metadata
6
+ performed_action :<%= action_name %>
6
7
 
7
8
  # Schema for validating params
8
9
  schema do
@@ -23,7 +24,11 @@ class <%= class_name %>::<%= action_name.camelize %>Service < BetterService::Ser
23
24
 
24
25
  # Phase 1: Search - Load the resource
25
26
  search_with do
27
+ <% if using_base_service? -%>
28
+ { resource: <%= singular_table_name %>_repository.find(params[:id]) }
29
+ <% else -%>
26
30
  { resource: user.<%= plural_table_name %>.find(params[:id]) }
31
+ <% end -%>
27
32
  end
28
33
 
29
34
  # Phase 2: Process - Perform the action
@@ -36,7 +41,7 @@ class <%= class_name %>::<%= action_name.camelize %>Service < BetterService::Ser
36
41
 
37
42
  # Phase 4: Respond - Format response (optional override)
38
43
  respond_with do |data|
39
- success_result("<%= human_name %> <%= action_name %> successfully", data)
44
+ success_result(message("<%= action_name %>.success"), data)
40
45
  end
41
46
  end
42
47
  <% end -%>
@@ -0,0 +1,53 @@
1
+ en:
2
+ <%= file_name %>:
3
+ services:
4
+ # Index service messages
5
+ index:
6
+ success: "<%= human_name.pluralize %> loaded successfully"
7
+ error: "Error loading <%= human_name.downcase.pluralize %>"
8
+ empty: "No <%= human_name.downcase.pluralize %> found"
9
+
10
+ # Show service messages
11
+ show:
12
+ success: "<%= human_name %> loaded successfully"
13
+ error: "Error loading <%= human_name.downcase %>"
14
+ not_found: "<%= human_name %> not found"
15
+
16
+ # Create service messages
17
+ create:
18
+ success: "<%= human_name %> created successfully"
19
+ error: "Error creating <%= human_name.downcase %>"
20
+ validation_failed: "Could not create <%= human_name.downcase %> due to validation errors"
21
+
22
+ # Update service messages
23
+ update:
24
+ success: "<%= human_name %> updated successfully"
25
+ error: "Error updating <%= human_name.downcase %>"
26
+ validation_failed: "Could not update <%= human_name.downcase %> due to validation errors"
27
+ not_found: "<%= human_name %> not found"
28
+
29
+ # Destroy service messages
30
+ destroy:
31
+ success: "<%= human_name %> deleted successfully"
32
+ error: "Error deleting <%= human_name.downcase %>"
33
+ not_found: "<%= human_name %> not found"
34
+ cannot_destroy: "Cannot delete this <%= human_name.downcase %>"
35
+
36
+ # Common messages (shared across services)
37
+ common:
38
+ success: "Operation completed successfully"
39
+ error: "An error occurred"
40
+ not_found: "<%= human_name %> not found"
41
+ not_authorized: "You are not authorized to perform this action"
42
+ invalid_params: "Invalid parameters provided"
43
+ already_exists: "<%= human_name %> already exists"
44
+
45
+ # Add custom action messages here:
46
+ # publish:
47
+ # success: "<%= human_name %> published successfully"
48
+ # error: "Error publishing <%= human_name.downcase %>"
49
+ # already_published: "<%= human_name %> is already published"
50
+ #
51
+ # archive:
52
+ # success: "<%= human_name %> archived successfully"
53
+ # error: "Error archiving <%= human_name.downcase %>"
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% module_namespacing do -%>
4
+ # BaseService for <%= class_name %> resource operations.
5
+ #
6
+ # Provides centralized configuration for all <%= class_name %> services:
7
+ # - Repository access via RepositoryAware concern
8
+ # - I18n messages namespace
9
+ # - Cache invalidation contexts
10
+ # - Presenter configuration
11
+ #
12
+ # All <%= class_name %> services should inherit from this class:
13
+ # class <%= class_name %>::IndexService < <%= class_name %>::BaseService
14
+ # class <%= class_name %>::CreateService < <%= class_name %>::BaseService
15
+ #
16
+ # @example Usage
17
+ # class <%= class_name %>::IndexService < <%= class_name %>::BaseService
18
+ # schema do
19
+ # optional(:page).filled(:integer, gteq?: 1)
20
+ # end
21
+ #
22
+ # search_with do
23
+ # { items: <%= singular_table_name %>_repository.all.to_a }
24
+ # end
25
+ # end
26
+ #
27
+ class <%= class_name %>::BaseService < BetterService::Services::Base
28
+ include BetterService::Concerns::Serviceable::RepositoryAware
29
+
30
+ # I18n messages namespace - messages are loaded from:
31
+ # config/locales/<%= file_name %>_services.en.yml
32
+ messages_namespace :<%= file_name %>
33
+
34
+ # Cache contexts for automatic invalidation
35
+ # Create/Update/Destroy services will invalidate these contexts
36
+ cache_contexts [:<%= plural_table_name %>]
37
+
38
+ <% unless options[:skip_presenter] -%>
39
+ # Presenter for transforming data in responses
40
+ # Generate with: rails generate better_service:presenter <%= class_name %>
41
+ # presenter <%= class_name %>Presenter
42
+ # presenter_options do
43
+ # { current_user: user }
44
+ # end
45
+
46
+ <% end -%>
47
+ <% unless options[:skip_repository] -%>
48
+ # Repository declaration - creates <%= singular_table_name %>_repository method
49
+ repository :<%= singular_table_name %>
50
+
51
+ # Add more repositories as needed:
52
+ # repository :user
53
+ # repository :category, class_name: "Categories::CategoryRepository"
54
+
55
+ <% end -%>
56
+ private
57
+
58
+ # Override to provide default error message for this resource
59
+ def default_error_message
60
+ message("common.error")
61
+ end
62
+
63
+ # Override to provide default success message for this resource
64
+ def default_success_message
65
+ message("common.success")
66
+ end
67
+
68
+ # Add shared helper methods for all <%= class_name %> services here:
69
+ #
70
+ # def find_<%= singular_table_name %>(id)
71
+ # <%= singular_table_name %>_repository.find(id)
72
+ # end
73
+ #
74
+ # def <%= singular_table_name %>_authorized?(record)
75
+ # record.user_id == user.id
76
+ # end
77
+ end
78
+ <% end -%>
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ <% module_namespacing do -%>
6
+ class <%= class_name %>::BaseServiceTest < ActiveSupport::TestCase
7
+ def setup
8
+ @user = users(:one) # Adjust fixture name as needed
9
+ end
10
+
11
+ # Test that the base service cannot be called directly
12
+ # (it requires subclass implementation)
13
+ test "base service requires schema definition" do
14
+ assert_raises(BetterService::Errors::Configuration::SchemaRequiredError) do
15
+ <%= class_name %>::BaseService.new(@user, params: {})
16
+ end
17
+ end
18
+
19
+ test "messages_namespace is configured" do
20
+ assert_equal :<%= file_name %>, <%= class_name %>::BaseService._messages_namespace
21
+ end
22
+
23
+ test "cache_contexts is configured" do
24
+ assert_includes <%= class_name %>::BaseService._cache_contexts, :<%= plural_table_name %>
25
+ end
26
+ <% unless options[:skip_repository] -%>
27
+
28
+ # Repository tests
29
+ # Note: These tests verify the repository declaration works
30
+ # Actual repository functionality is tested in repository_test.rb
31
+
32
+ test "repository is accessible via method" do
33
+ # Create a concrete subclass to test repository access
34
+ test_service_class = Class.new(<%= class_name %>::BaseService) do
35
+ schema { optional(:id).filled }
36
+
37
+ def test_repository_access
38
+ <%= singular_table_name %>_repository
39
+ end
40
+ end
41
+
42
+ service = test_service_class.new(@user, params: {})
43
+ repo = service.send(:test_repository_access)
44
+
45
+ assert_instance_of <%= class_name %>Repository, repo
46
+ end
47
+
48
+ test "repository is memoized" do
49
+ test_service_class = Class.new(<%= class_name %>::BaseService) do
50
+ schema { optional(:id).filled }
51
+
52
+ def test_repository_memoization
53
+ [<%= singular_table_name %>_repository, <%= singular_table_name %>_repository]
54
+ end
55
+ end
56
+
57
+ service = test_service_class.new(@user, params: {})
58
+ repos = service.send(:test_repository_memoization)
59
+
60
+ assert_same repos[0], repos[1], "Repository should be memoized"
61
+ end
62
+ <% end -%>
63
+ end
64
+ <% end -%>
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  <% module_namespacing do -%>
4
- class <%= class_name %>::CreateService < BetterService::Services::CreateService
5
- # Database transactions are ENABLED by default for create operations
6
- # The process block will automatically run inside a transaction
7
- # To disable: with_transaction false
4
+ class <%= class_name %>::CreateService < <%= parent_class %>
5
+ # Action name for metadata
6
+ performed_action :created
7
+
8
+ # Enable transaction wrapping
9
+ with_transaction true
8
10
 
9
11
  # Schema for validating params
10
12
  schema do
@@ -20,24 +22,33 @@ class <%= class_name %>::CreateService < BetterService::Services::CreateService
20
22
  end
21
23
 
22
24
  # Phase 2: Process - Create the resource (runs in transaction)
25
+ # Uses save (not save!) to allow graceful validation handling
23
26
  process_with do |data|
24
- <%= file_name %> = user.<%= plural_table_name %>.create!(params)
25
- { resource: <%= file_name %> }
27
+ <% if using_base_service? -%>
28
+ record = <%= singular_table_name %>_repository.build(params)
29
+
30
+ if record.save
31
+ { object: record }
32
+ else
33
+ failure_for(record)
34
+ end
35
+ <% else -%>
36
+ <%= file_name %> = user.<%= plural_table_name %>.build(params)
37
+
38
+ if <%= file_name %>.save
39
+ { object: <%= file_name %> }
40
+ else
41
+ failure_for(<%= file_name %>)
42
+ end
43
+ <% end -%>
26
44
  end
27
45
 
28
- # Phase 4: Respond - Format response (optional override)
29
- # Uses I18n message helper with fallback to default messages
30
- #
31
- # To customize messages, create config/locales/<%= plural_table_name %>_services.en.yml:
32
- # en:
33
- # <%= plural_table_name %>:
34
- # services:
35
- # create:
36
- # success: "<%= human_name %> created successfully!"
37
- #
38
- # Then configure namespace: messages_namespace :<%= plural_table_name %>
46
+ # Phase 4: Respond - Format response
39
47
  respond_with do |data|
40
- success_result(message("create.success"), data)
48
+ # Pass through failure responses
49
+ return data if data[:success] == false
50
+
51
+ success_for(data[:object], message("create.success"))
41
52
  end
42
53
  end
43
54
  <% end -%>
@@ -1,50 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  <% module_namespacing do -%>
4
- class <%= class_name %>::DestroyService < BetterService::Services::DestroyService
5
- # Database transactions are ENABLED by default for destroy operations
6
- # The process block will automatically run inside a transaction
7
- # To disable: with_transaction false
4
+ class <%= class_name %>::DestroyService < <%= parent_class %>
5
+ # Action name for metadata
6
+ performed_action :destroyed
7
+
8
+ # Enable transaction wrapping
9
+ with_transaction true
8
10
 
9
11
  # Schema for validating params
10
12
  schema do
11
13
  required(:id).filled
12
14
  end
13
15
 
14
- # Authorization (optional) - Runs BEFORE search phase
15
- # Uncomment and customize for your needs:
16
- #
17
- # authorize_with do
18
- # # Example: Check ownership or admin role
19
- # <%= file_name %> = <%= class_name %>.find(params[:id])
20
- # user.admin? || <%= file_name %>.user_id == user.id
21
- # end
22
-
23
16
  # Phase 1: Search - Load the resource
24
17
  search_with do
25
- { resource: user.<%= plural_table_name %>.find(params[:id]) }
18
+ <% if using_base_service? -%>
19
+ { object: <%= singular_table_name %>_repository.find(params[:id]) }
20
+ <% else -%>
21
+ { object: user.<%= plural_table_name %>.find(params[:id]) }
22
+ <% end -%>
26
23
  end
27
24
 
28
25
  # Phase 2: Process - Delete the resource (runs in transaction)
29
26
  process_with do |data|
30
- <%= file_name %> = data[:resource]
31
- <%= file_name %>.destroy!
32
- { resource: <%= file_name %> }
27
+ record = data[:object]
28
+ record.destroy!
29
+ { object: record }
33
30
  end
34
31
 
35
- # Phase 4: Respond - Format response (optional override)
36
- # Uses I18n message helper with fallback to default messages
37
- #
38
- # To customize messages, create config/locales/<%= plural_table_name %>_services.en.yml:
39
- # en:
40
- # <%= plural_table_name %>:
41
- # services:
42
- # destroy:
43
- # success: "<%= human_name %> deleted successfully!"
44
- #
45
- # Then configure namespace: messages_namespace :<%= plural_table_name %>
32
+ # Phase 4: Respond - Format response
46
33
  respond_with do |data|
47
- success_result(message("destroy.success"), data)
34
+ success_for(data[:object], message("destroy.success"))
48
35
  end
49
36
  end
50
37
  <% end -%>
@@ -1,14 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  <% module_namespacing do -%>
4
- class <%= class_name %>::IndexService < BetterService::Services::IndexService
5
- # Optional: Use a presenter to format response data
6
- # Generate with: rails generate better_service:presenter <%= class_name %>
7
- #
8
- # presenter <%= class_name %>Presenter
9
- # presenter_options do
10
- # { current_user: user }
11
- # end
4
+ class <%= class_name %>::IndexService < <%= parent_class %>
5
+ # Action name for metadata
6
+ performed_action :listed
12
7
 
13
8
  # Schema for validating params
14
9
  schema do
@@ -19,21 +14,29 @@ class <%= class_name %>::IndexService < BetterService::Services::IndexService
19
14
 
20
15
  # Phase 1: Search - Load raw data
21
16
  search_with do
17
+ <% if using_base_service? -%>
18
+ # Use repository for data access
19
+ predicates = {}
20
+ predicates[:search] = params[:search] if params[:search].present?
21
+
22
+ { object: <%= singular_table_name %>_repository.search(predicates, page: params[:page] || 1, per_page: params[:per_page] || 25).to_a }
23
+ <% else -%>
22
24
  <%= plural_table_name %> = user.<%= plural_table_name %>
23
25
  <%= plural_table_name %> = <%= plural_table_name %>.where("title LIKE ?", "%#{params[:search]}%") if params[:search].present?
24
26
  # Add pagination if needed (e.g., with Kaminari or Pagy)
25
27
  # <%= plural_table_name %> = <%= plural_table_name %>.page(params[:page]).per(params[:per_page] || 25)
26
28
 
27
- { items: <%= plural_table_name %>.to_a }
29
+ { object: <%= plural_table_name %>.to_a }
30
+ <% end -%>
28
31
  end
29
32
 
30
33
  # Phase 2: Process - Transform and aggregate data
31
34
  process_with do |data|
32
35
  {
33
- items: data[:items],
36
+ object: data[:object],
34
37
  metadata: {
35
38
  stats: {
36
- total: data[:items].count
39
+ total: data[:object].count
37
40
  },
38
41
  pagination: {
39
42
  page: params[:page] || 1,
@@ -43,30 +46,9 @@ class <%= class_name %>::IndexService < BetterService::Services::IndexService
43
46
  }
44
47
  end
45
48
 
46
- # Phase 4: Respond - Format response (optional override)
47
- # Uses I18n message helper with fallback to default messages
48
- #
49
- # To customize messages, create config/locales/<%= plural_table_name %>_services.en.yml:
50
- # en:
51
- # <%= plural_table_name %>:
52
- # services:
53
- # index:
54
- # success: "<%= plural_table_name.titleize %> loaded successfully!"
55
- #
56
- # Then configure namespace: messages_namespace :<%= plural_table_name %>
49
+ # Phase 4: Respond - Format response
57
50
  respond_with do |data|
58
- success_result(message("index.success"), data)
51
+ success_for(data[:object], message("index.success")).merge(metadata: data[:metadata])
59
52
  end
60
-
61
- # Phase 5: Viewer - UI configuration (optional)
62
- # viewer do |processed, transformed, result|
63
- # {
64
- # page_title: "<%= plural_table_name.titleize %>",
65
- # breadcrumbs: [
66
- # { label: "Home", url: "/" },
67
- # { label: "<%= plural_table_name.titleize %>", url: "/<%= plural_table_name %>" }
68
- # ]
69
- # }
70
- # end
71
53
  end
72
54
  <% end -%>
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% module_namespacing do -%>
4
+ # Repository for <%= class_name %> model data access.
5
+ #
6
+ # Provides a clean abstraction layer between services and ActiveRecord.
7
+ # The model class is automatically inferred from the repository name:
8
+ # <%= class_name %>Repository -> <%= class_name %>
9
+ #
10
+ # Inherited methods from BaseRepository:
11
+ # - find(id), find_by(attributes), where(conditions)
12
+ # - create(attributes), create!(attributes)
13
+ # - update(record, attributes), update!(record, attributes)
14
+ # - destroy(record), destroy!(record)
15
+ # - search(predicates, page:, per_page:, includes:, order:)
16
+ # - all, count, exists?
17
+ #
18
+ # @example Basic usage
19
+ # repo = <%= class_name %>Repository.new
20
+ # repo.find(1)
21
+ # repo.search({ status_eq: 'active' }, page: 1, per_page: 20)
22
+ #
23
+ # @example In services (via RepositoryAware)
24
+ # class <%= class_name %>::IndexService < <%= class_name %>::BaseService
25
+ # search_with do
26
+ # { items: <%= singular_table_name %>_repository.active.recent.to_a }
27
+ # end
28
+ # end
29
+ #
30
+ class <%= class_name %>Repository < BetterService::Repository::BaseRepository
31
+ # Model is inferred automatically: <%= class_name %>Repository -> <%= class_name %>
32
+ # Override with explicit model if needed:
33
+ # def initialize(model_class = <%= class_name %>)
34
+ # super
35
+ # end
36
+
37
+ # Add custom repository methods below:
38
+
39
+ # @example Scope methods
40
+ # def active
41
+ # where(active: true)
42
+ # end
43
+ #
44
+ # def inactive
45
+ # where(active: false)
46
+ # end
47
+
48
+ # @example Ownership methods
49
+ # def for_user(user)
50
+ # where(user_id: user.id)
51
+ # end
52
+ #
53
+ # def for_organization(org)
54
+ # where(organization_id: org.id)
55
+ # end
56
+
57
+ # @example Ordering methods
58
+ # def recent(limit = 10)
59
+ # model.order(created_at: :desc).limit(limit)
60
+ # end
61
+ #
62
+ # def by_name
63
+ # model.order(:name)
64
+ # end
65
+
66
+ # @example Complex queries
67
+ # def with_associations
68
+ # model.includes(:user, :category)
69
+ # end
70
+ #
71
+ # def published_today
72
+ # where(published: true)
73
+ # .where("published_at >= ?", Time.current.beginning_of_day)
74
+ # end
75
+ end
76
+ <% end -%>
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ <% module_namespacing do -%>
6
+ class <%= class_name %>RepositoryTest < ActiveSupport::TestCase
7
+ def setup
8
+ @repository = <%= class_name %>Repository.new
9
+ end
10
+
11
+ # Model inference tests
12
+ test "repository infers model class correctly" do
13
+ assert_equal <%= class_name %>, @repository.model
14
+ end
15
+
16
+ # Basic CRUD operation tests
17
+ # Uncomment and adjust based on your fixtures/factories
18
+
19
+ # test "all returns collection" do
20
+ # records = @repository.all
21
+ # assert_respond_to records, :each
22
+ # assert_kind_of ActiveRecord::Relation, records
23
+ # end
24
+
25
+ # test "find returns record by id" do
26
+ # record = <%= plural_table_name %>(:one)
27
+ # found = @repository.find(record.id)
28
+ # assert_equal record, found
29
+ # end
30
+
31
+ # test "find raises for non-existent record" do
32
+ # assert_raises(ActiveRecord::RecordNotFound) do
33
+ # @repository.find(-1)
34
+ # end
35
+ # end
36
+
37
+ # test "find_by returns matching record" do
38
+ # record = <%= plural_table_name %>(:one)
39
+ # found = @repository.find_by(id: record.id)
40
+ # assert_equal record, found
41
+ # end
42
+
43
+ # test "find_by returns nil for no match" do
44
+ # found = @repository.find_by(id: -1)
45
+ # assert_nil found
46
+ # end
47
+
48
+ # test "where returns filtered collection" do
49
+ # records = @repository.where(active: true)
50
+ # assert_kind_of ActiveRecord::Relation, records
51
+ # end
52
+
53
+ # test "count returns number of records" do
54
+ # count = @repository.count
55
+ # assert_kind_of Integer, count
56
+ # end
57
+
58
+ # test "exists? returns boolean" do
59
+ # assert_includes [true, false], @repository.exists?(id: 1)
60
+ # end
61
+
62
+ # Create operation tests
63
+
64
+ # test "create persists new record" do
65
+ # assert_difference '<%= class_name %>.count' do
66
+ # @repository.create(valid_attributes)
67
+ # end
68
+ # end
69
+
70
+ # test "create! raises on invalid attributes" do
71
+ # assert_raises(ActiveRecord::RecordInvalid) do
72
+ # @repository.create!(invalid_attributes)
73
+ # end
74
+ # end
75
+
76
+ # Update operation tests
77
+
78
+ # test "update modifies record" do
79
+ # record = <%= plural_table_name %>(:one)
80
+ # @repository.update(record, name: "Updated Name")
81
+ # record.reload
82
+ # assert_equal "Updated Name", record.name
83
+ # end
84
+
85
+ # Destroy operation tests
86
+
87
+ # test "destroy removes record" do
88
+ # record = <%= plural_table_name %>(:one)
89
+ # assert_difference '<%= class_name %>.count', -1 do
90
+ # @repository.destroy(record)
91
+ # end
92
+ # end
93
+
94
+ # Search operation tests (if using predicates)
95
+
96
+ # test "search with predicates returns filtered results" do
97
+ # results = @repository.search({ active_eq: true }, limit: nil)
98
+ # assert results.all?(&:active)
99
+ # end
100
+
101
+ # test "search with pagination" do
102
+ # results = @repository.search({}, page: 1, per_page: 10)
103
+ # assert results.size <= 10
104
+ # end
105
+
106
+ private
107
+
108
+ # Define valid attributes for create tests
109
+ # def valid_attributes
110
+ # {
111
+ # name: "Test <%= human_name %>",
112
+ # # Add other required attributes
113
+ # }
114
+ # end
115
+
116
+ # Define invalid attributes for validation tests
117
+ # def invalid_attributes
118
+ # {
119
+ # name: nil,
120
+ # # Add attributes that should fail validation
121
+ # }
122
+ # end
123
+ end
124
+ <% end -%>