rider-kick 0.0.12 → 0.0.14

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +668 -27
  3. data/lib/generators/rider_kick/USAGE +2 -0
  4. data/lib/generators/rider_kick/base_generator.rb +190 -0
  5. data/lib/generators/rider_kick/clean_arch_generator.rb +235 -44
  6. data/lib/generators/rider_kick/clean_arch_generator_engine_spec.rb +359 -0
  7. data/lib/generators/rider_kick/clean_arch_generator_factory_bot_spec.rb +131 -0
  8. data/lib/generators/rider_kick/entity_type_mapping_spec.rb +61 -0
  9. data/lib/generators/rider_kick/errors.rb +42 -0
  10. data/lib/generators/rider_kick/factory_generator.rb +238 -0
  11. data/lib/generators/rider_kick/factory_generator_spec.rb +175 -0
  12. data/lib/generators/rider_kick/repositories_contract_spec.rb +135 -0
  13. data/lib/generators/rider_kick/scaffold_generator.rb +377 -62
  14. data/lib/generators/rider_kick/scaffold_generator_builder_uploaders_spec.rb +159 -0
  15. data/lib/generators/rider_kick/scaffold_generator_conditional_filtering_spec.rb +820 -0
  16. data/lib/generators/rider_kick/scaffold_generator_contracts_spec.rb +96 -0
  17. data/lib/generators/rider_kick/scaffold_generator_contracts_with_scope_spec.rb +83 -0
  18. data/lib/generators/rider_kick/scaffold_generator_engine_spec.rb +221 -0
  19. data/lib/generators/rider_kick/scaffold_generator_idempotent_spec.rb +84 -0
  20. data/lib/generators/rider_kick/scaffold_generator_list_spec_format_spec.rb +153 -0
  21. data/lib/generators/rider_kick/scaffold_generator_rspec_spec.rb +347 -0
  22. data/lib/generators/rider_kick/scaffold_generator_success_spec.rb +101 -0
  23. data/lib/generators/rider_kick/scaffold_generator_with_scope_spec.rb +76 -0
  24. data/lib/generators/rider_kick/structure_generator.rb +179 -35
  25. data/lib/generators/rider_kick/structure_generator_comprehensive_spec.rb +598 -0
  26. data/lib/generators/rider_kick/structure_generator_engine_spec.rb +279 -0
  27. data/lib/generators/rider_kick/structure_generator_spec.rb +20 -0
  28. data/lib/generators/rider_kick/structure_generator_success_spec.rb +64 -0
  29. data/lib/generators/rider_kick/structure_generator_unit_spec.rb +2202 -0
  30. data/lib/generators/rider_kick/templates/.rubocop.yml +5 -4
  31. data/lib/generators/rider_kick/templates/config/initializers/version.rb.tt +1 -1
  32. data/lib/generators/rider_kick/templates/db/migrate/20220613145533_init_database.rb +1 -1
  33. data/lib/generators/rider_kick/templates/db/structures/example.yaml.tt +157 -51
  34. data/lib/generators/rider_kick/templates/domains/core/builders/builder.rb.tt +36 -10
  35. data/lib/generators/rider_kick/templates/domains/core/builders/builder_spec.rb.tt +219 -0
  36. data/lib/generators/rider_kick/templates/domains/core/builders/error.rb.tt +2 -2
  37. data/lib/generators/rider_kick/templates/domains/core/builders/pagination.rb.tt +2 -2
  38. data/lib/generators/rider_kick/templates/domains/core/entities/entity.rb.tt +32 -14
  39. data/lib/generators/rider_kick/templates/domains/core/entities/error.rb.tt +1 -1
  40. data/lib/generators/rider_kick/templates/domains/core/entities/pagination.rb.tt +1 -1
  41. data/lib/generators/rider_kick/templates/domains/core/repositories/abstract_repository.rb.tt +4 -16
  42. data/lib/generators/rider_kick/templates/domains/core/repositories/create.rb.tt +2 -2
  43. data/lib/generators/rider_kick/templates/domains/core/repositories/create_spec.rb.tt +78 -0
  44. data/lib/generators/rider_kick/templates/domains/core/repositories/destroy.rb.tt +2 -2
  45. data/lib/generators/rider_kick/templates/domains/core/repositories/destroy_spec.rb.tt +88 -0
  46. data/lib/generators/rider_kick/templates/domains/core/repositories/fetch_by_id.rb.tt +3 -3
  47. data/lib/generators/rider_kick/templates/domains/core/repositories/fetch_by_id_spec.rb.tt +62 -0
  48. data/lib/generators/rider_kick/templates/domains/core/repositories/list.rb.tt +12 -7
  49. data/lib/generators/rider_kick/templates/domains/core/repositories/list_spec.rb.tt +190 -0
  50. data/lib/generators/rider_kick/templates/domains/core/repositories/update.rb.tt +4 -4
  51. data/lib/generators/rider_kick/templates/domains/core/repositories/update_spec.rb.tt +119 -0
  52. data/lib/generators/rider_kick/templates/domains/core/use_cases/contract/default.rb.tt +1 -1
  53. data/lib/generators/rider_kick/templates/domains/core/use_cases/contract/pagination.rb.tt +1 -1
  54. data/lib/generators/rider_kick/templates/domains/core/use_cases/create.rb.tt +3 -7
  55. data/lib/generators/rider_kick/templates/domains/core/use_cases/create_spec.rb.tt +71 -0
  56. data/lib/generators/rider_kick/templates/domains/core/use_cases/destroy.rb.tt +3 -7
  57. data/lib/generators/rider_kick/templates/domains/core/use_cases/destroy_spec.rb.tt +62 -0
  58. data/lib/generators/rider_kick/templates/domains/core/use_cases/fetch_by_id.rb.tt +3 -7
  59. data/lib/generators/rider_kick/templates/domains/core/use_cases/fetch_by_id_spec.rb.tt +62 -0
  60. data/lib/generators/rider_kick/templates/domains/core/use_cases/get_version.rb.tt +2 -2
  61. data/lib/generators/rider_kick/templates/domains/core/use_cases/list.rb.tt +3 -7
  62. data/lib/generators/rider_kick/templates/domains/core/use_cases/list_spec.rb.tt +64 -0
  63. data/lib/generators/rider_kick/templates/domains/core/use_cases/update.rb.tt +3 -7
  64. data/lib/generators/rider_kick/templates/domains/core/use_cases/update_spec.rb.tt +73 -0
  65. data/lib/generators/rider_kick/templates/domains/core/utils/abstract_utils.rb.tt +29 -0
  66. data/lib/generators/rider_kick/templates/domains/core/utils/request_methods.rb.tt +3 -2
  67. data/lib/generators/rider_kick/templates/env.development +1 -1
  68. data/lib/generators/rider_kick/templates/env.production +1 -1
  69. data/lib/generators/rider_kick/templates/env.test +1 -1
  70. data/lib/generators/rider_kick/templates/models/{application_record.rb → application_record.rb.tt} +3 -1
  71. data/lib/generators/rider_kick/templates/models/model_spec.rb.tt +68 -0
  72. data/lib/generators/rider_kick/templates/spec/factories/.gitkeep +19 -0
  73. data/lib/generators/rider_kick/templates/spec/factories/factory.rb.tt +8 -0
  74. data/lib/generators/rider_kick/templates/spec/rails_helper.rb +2 -0
  75. data/lib/generators/rider_kick/templates/spec/support/class_stubber.rb +148 -0
  76. data/lib/generators/rider_kick/templates/spec/support/factory_bot.rb +34 -0
  77. data/lib/generators/rider_kick/templates/spec/support/faker.rb +61 -0
  78. data/lib/rider-kick.rb +9 -6
  79. data/lib/rider_kick/builders/abstract_active_record_entity_builder_spec.rb +596 -68
  80. data/lib/rider_kick/configuration.rb +238 -0
  81. data/lib/rider_kick/configuration_engine_spec.rb +377 -0
  82. data/lib/rider_kick/entities/failure_details.rb +23 -15
  83. data/lib/rider_kick/entities/failure_details_spec.rb +22 -0
  84. data/lib/rider_kick/matchers/use_case_result.rb +1 -1
  85. data/lib/rider_kick/matchers/use_case_result_edge_spec.rb +28 -0
  86. data/lib/rider_kick/use_cases/abstract_use_case.rb +1 -1
  87. data/lib/rider_kick/use_cases/abstract_use_case_spec.rb +57 -0
  88. data/lib/rider_kick/version.rb +1 -1
  89. metadata +345 -52
  90. data/.rspec +0 -3
  91. data/.rubocop.yml +0 -1141
  92. data/CHANGELOG.md +0 -5
  93. data/Rakefile +0 -12
  94. data/lib/rider_kick/matchers/use_case_result_spec.rb +0 -64
@@ -9,7 +9,6 @@ inherit_gem: { rubocop-rails-omakase: rubocop.yml }
9
9
 
10
10
  AllCops:
11
11
  SuggestExtensions: false
12
- TargetRubyVersion: 3.3.4
13
12
  DisabledByDefault: true
14
13
  Exclude:
15
14
  - '**/templates/**/*'
@@ -20,8 +19,11 @@ AllCops:
20
19
  - 'db/**/*'
21
20
  - 'config/**/*'
22
21
  - 'script/**/*'
22
+ - 'bin/*'
23
23
  - 'public/views/*'
24
24
  - 'bin/**/*'
25
+ - 'config/**/*'
26
+ - 'db/**/*'
25
27
  - 'tmp/**/*'
26
28
  - 'spec/spec_helper.rb'
27
29
  - 'spec/rails_helper.rb'
@@ -1092,9 +1094,6 @@ Style/CommandLiteral:
1092
1094
  Style/ConstantVisibility:
1093
1095
  Enabled: true
1094
1096
 
1095
- Style/ClassAndModuleChildren:
1096
- Enabled: false
1097
-
1098
1097
  Style/Documentation:
1099
1098
  Enabled: false
1100
1099
 
@@ -1134,3 +1133,5 @@ Naming/MethodParameterName:
1134
1133
  Layout/ClassStructure:
1135
1134
  Enabled: true
1136
1135
 
1136
+ Style/WordArray:
1137
+ EnforcedStyle: brackets
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module <%= Rails.application.class.module_parent_name %>
3
+ module <%= Rails.application&.class&.module_parent_name || 'MyApp' %>
4
4
  VERSION = '0.0.1'
5
5
  public_constant :VERSION
6
6
  end
@@ -1,4 +1,4 @@
1
- class InitDatabase < ActiveRecord::Migration[7.2]
1
+ class InitDatabase < ActiveRecord::Migration[8.1]
2
2
  def change
3
3
  enable_extension 'pgcrypto'
4
4
  end
@@ -1,100 +1,206 @@
1
+ # RiderKick Structure Definition for <%= @scope_class %>
2
+ # This file acts as a centralized contract for code generation.
3
+ # Modifying this file and re-running 'rider_kick:scaffold' will update the domain logic.
4
+
1
5
  model: <%= @model_class %>
2
6
  resource_name: <%= @scope_path %>
3
- resource_owner_id: <%= @resource_owner_id %>
4
- actor: <%= @actor %>
7
+ resource_owner_id: <%= @resource_owner_id %> # account_id
8
+ resource_owner: <%= @resource_owner %> # account
9
+ actor: <%= @actor %> # user
10
+ actor_id: <%= @actor_id %> # user_id
11
+
5
12
  fields:
6
- <% @model_class.columns.each do |field| -%>
7
- - <%= field.name.to_s %>
13
+ <% @fields.each do |f| -%>
14
+ - <%= f %>
8
15
  <% end -%>
9
- <% @uploaders.each do |field| -%>
10
- - <%= field %>
16
+ <% @uploaders.each do |f| -%>
17
+ - <%= f %>
11
18
  <% end -%>
19
+
20
+ <% if entity_uploader_definitions.empty? -%>
21
+ uploaders: []
22
+ <% else -%>
12
23
  uploaders:
13
- <% @uploaders.each do |field| -%>
14
- - <%= field %>
24
+ <% entity_uploader_definitions.each do |uploader_hash| -%>
25
+ - { name: '<%= uploader_hash[:name] %>', type: '<%= uploader_hash[:type] %>' }
26
+ <% end -%>
15
27
  <% end -%>
28
+
29
+ <% search_able_fields = arg_settings['search_able'].to_s.split(',').map(&:strip).reject(&:blank?) -%>
30
+ <% if search_able_fields.empty? -%>
31
+ search_able: []
32
+ <% else -%>
16
33
  search_able:
17
- <% contract_fields.each do |field| -%>
18
- <% if ['title', 'name'].include?(field) -%>
19
- - <%= field %>
34
+ <% search_able_fields.each do |f| -%>
35
+ - <%= f %>
36
+ <% end -%>
37
+ <% end -%>
38
+
39
+ # ---- Enriched metadata (opsional, untuk tooling/insight) ----
40
+ schema:
41
+ columns:
42
+ <% if columns_meta.empty? -%>
43
+ []
44
+ <% else -%>
45
+ <% columns_meta.each do |c| -%>
46
+ - name: <%= c[:name] %>
47
+ type: <%= c[:type] %>
48
+ sql_type: <%= c[:sql_type] || 'null' %>
49
+ null: <%= c[:null] || 'false' %>
50
+ <% if c[:default].present? -%>
51
+ default: <%= c[:default].inspect %>
52
+ <% end -%>
53
+ <% if c[:precision] || c[:scale] -%>
54
+ precision: <%= c[:precision] || 'null' %>
55
+ scale: <%= c[:scale] || 'null' %>
56
+ <% end -%>
57
+ <% if c[:limit] -%>
58
+ limit: <%= c[:limit] %>
59
+ <% end -%>
60
+ <% end -%>
61
+ <% end -%>
62
+ foreign_keys:
63
+ <% (fkeys_meta.presence || []).each do |fk| -%>
64
+ - column: <%= fk[:column] %>
65
+ to_table: <%= fk[:to_table] %>
20
66
  <% end -%>
67
+ <% if fkeys_meta.empty? -%>
68
+ []
21
69
  <% end -%>
70
+ indexes:
71
+ <% (indexes_meta.presence || []).each do |ix| -%>
72
+ - columns: [<%= ix[:columns].join(', ') %>]
73
+ unique: <%= ix[:unique] %>
74
+ <% end -%>
75
+ <% if indexes_meta.empty? -%>
76
+ []
77
+ <% end -%>
78
+ enums:
79
+ <% if enums_meta.present? -%>
80
+ <% enums_meta.each do |name, map| -%>
81
+ <%= name %>: <%= map.keys %>
82
+ <% end -%>
83
+ <% else -%>
84
+ {}
85
+ <% end -%>
86
+
22
87
  controllers:
23
88
  list_fields:
24
- <% @fields.each do |field| -%>
25
- - <%= field %>
89
+ <% @fields.each do |f| -%>
90
+ - <%= f %>
91
+ <% end -%>
92
+ <% if @fields.empty? -%>
93
+ []
26
94
  <% end -%>
27
95
  show_fields:
28
- <% @model_class.columns.each do |field| -%>
29
- - <%= field.name.to_s %>
96
+ <% (@columns.map { |c| c[:name] } + @uploaders).uniq.each do |f| -%>
97
+ - <%= f %>
30
98
  <% end -%>
31
- <% @uploaders.each do |field| -%>
32
- - <%= field %>
99
+ <% if (@columns.map { |c| c[:name] } + @uploaders).uniq.empty? -%>
100
+ []
33
101
  <% end -%>
34
102
  form_fields:
35
- <% (@fields).each do |field| -%>
36
- - name: <%= field %>
37
- type: string
38
- <% end -%>
39
- <% (@uploaders).each do |field| -%>
40
- <% if is_singular?(field) -%>
41
- - name: <%= field %>
42
- type: file
103
+ <% if @fields.empty? && @uploaders.empty? -%>
104
+ []
43
105
  <% else -%>
44
- - name: <%= field %>
45
- type: files
106
+ <% @fields.each do |f| -%>
107
+ <% if @uploaders.include?(f) -%>
108
+ - name: <%= f %>
109
+ type: <%= is_singular?(f) ? 'file' : 'files' %>
110
+ <% else -%>
111
+ - name: <%= f %>
112
+ type: <%= get_column_type(f) %>
113
+ <% end -%>
46
114
  <% end -%>
115
+ <% @uploaders.each do |f| -%>
116
+ <% unless @fields.include?(f) -%>
117
+ - name: <%= f %>
118
+ type: <%= is_singular?(f) ? 'file' : 'files' %>
47
119
  <% end -%>
120
+ <% end -%>
121
+ <% end -%>
122
+
48
123
  domains:
49
124
  action_list:
50
125
  use_case:
51
126
  contract:
127
+ <% if contract_lines_for_list.empty? -%>
128
+ []
129
+ <% else -%>
130
+ <% contract_lines_for_list.each do |line| -%>
131
+ - <%= line %>
132
+ <% end -%>
133
+ <% end -%>
134
+ repository:
135
+ filters:
136
+ <% if repository_list_filters.empty? -%>
137
+ []
138
+ <% else -%>
139
+ <% repository_list_filters.each do |line| -%>
140
+ - <%= line %>
141
+ <% end -%>
142
+ <% end -%>
143
+
52
144
  action_fetch_by_id:
53
145
  use_case:
54
146
  contract:
55
- - required(:id).filled(:string)
147
+ - "required(:id).filled(:string)"
148
+ <% if @resource_owner_id.present? -%>
149
+ - "required(:<%= @resource_owner_id %>).filled(:string)"
150
+ <% end -%>
151
+ <% search_able_fields = arg_settings['search_able'].to_s.split(',').map(&:strip).reject(&:blank?) -%>
152
+ <% if search_able_fields.any? -%>
153
+ <% search_able_fields.each do |f| -%>
154
+ # optional search fields: <%= f %>
155
+ <% end -%>
156
+ <% end -%>
157
+
56
158
  action_create:
57
159
  use_case:
58
160
  contract:
59
- <% (@fields + @uploaders).each do |field| -%>
60
- <% column_type = get_column_type(field) -%>
61
- <% dry_type = @type_mapping[column_type.to_s] || ':string' -%>
62
- <% if @uploaders.include?(field) -%>
63
- <% if is_singular?(field) -%>
64
- - optional(:<%= field %>).maybe(<%= dry_type %>)
65
- <% else -%>
66
- - optional(:<%= field %>).maybe(:array)
161
+ <% if @resource_owner_id.present? -%>
162
+ - "required(:<%= @resource_owner_id %>).filled(:string)"
67
163
  <% end -%>
164
+ <% if contract_lines_for_create.empty? && !@resource_owner_id.present? -%>
165
+ []
68
166
  <% else -%>
69
- - required(:<%= field %>).filled(<%= dry_type %>)
167
+ <% contract_lines_for_create.each do |line| -%>
168
+ - <%= line %>
70
169
  <% end -%>
71
170
  <% end -%>
171
+
72
172
  action_update:
73
173
  use_case:
74
174
  contract:
75
- - required(:id).filled(:string)
76
- <% (@fields + @uploaders).each do |field| -%>
77
- <% column_type = get_column_type(field) -%>
78
- <% dry_type = @type_mapping[column_type.to_s] || ':string' -%>
79
- <% if @uploaders.include?(field) -%>
80
- <% if is_singular?(field) -%>
81
- - optional(:<%= field %>).maybe(<%= dry_type %>)
82
- <% else -%>
83
- - optional(:<%= field %>).maybe(:array)
84
- <% end -%>
85
- <% else -%>
86
- - optional(:<%= field %>).maybe(<%= dry_type %>)
175
+ - "required(:id).filled(:string)"
176
+ <% if @resource_owner_id.present? -%>
177
+ - "required(:<%= @resource_owner_id %>).filled(:string)"
87
178
  <% end -%>
179
+ <% contract_lines_for_update.each do |line| -%>
180
+ - <%= line %>
88
181
  <% end -%>
182
+
89
183
  action_destroy:
90
184
  use_case:
91
185
  contract:
92
- - required(:id).filled(:string)
186
+ - "required(:id).filled(:string)"
187
+ <% if @resource_owner_id.present? -%>
188
+ - "required(:<%= @resource_owner_id %>).filled(:string)"
189
+ <% end -%>
190
+
93
191
  entity:
94
192
  skipped_fields:
95
193
  - id
96
194
  - created_at
97
195
  - updated_at
98
- <% if @model_class.columns.map(&:name).include?(:type) -%>
196
+ <% if @columns.map { |c| c[:name] }.include?('type') -%>
99
197
  - type
198
+ <% end -%>
199
+ db_attributes:
200
+ <% if entity_db_fields.empty? -%>
201
+ []
202
+ <% else -%>
203
+ <% entity_db_fields.each do |field| -%>
204
+ - <%= field %>
205
+ <% end -%>
100
206
  <% end -%>
@@ -1,25 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Core::Builders::<%= @subject_class %> < RiderKick::Builders::AbstractActiveRecordEntityBuilder
4
- acts_as_builder_for_entity Core::Entities::<%= @subject_class%>
3
+ class <%= domain_class_name %>::Builders::<%= @subject_class %> < RiderKick::Builders::AbstractActiveRecordEntityBuilder
4
+ acts_as_builder_for_entity <%= domain_class_name %>::Entities::<%= @subject_class%>
5
5
 
6
6
  <% if @uploaders.present? -%>
7
7
  def attributes_for_entity
8
8
  {
9
- <% @uploaders.each_with_index do |field, index| -%>
10
- <% if is_singular?(field) -%>
11
- <%= field %>: (Rails.application.routes.url_helpers.polymorphic_url(params.<%= field %>) rescue '')<%= ',' if index < @uploaders.size - 1 %>
9
+ <% @uploaders.each_with_index do |uploader, index| -%>
10
+ <% if uploader.type == 'single' -%>
11
+ <%= uploader.name %>_url: with_<%= uploader.name %>_url(@params)<%= index < @uploaders.length - 1 ? ',' : '' %>
12
12
  <% else -%>
13
- <%= field %>: build_assets(params.<%= field %>)<%= ',' if index < @uploaders.size - 1 %>
13
+ <%= uploader.name %>_urls: with_<%= uploader.name %>_urls(@params)<%= index < @uploaders.length - 1 ? ',' : '' %>
14
14
  <% end -%>
15
15
  <% end -%>
16
16
  }
17
17
  end
18
18
 
19
- private
19
+ private
20
+ <% end -%>
21
+ <%- @uploaders.each do |uploader| -%>
22
+ <%- if uploader.type == 'single' -%>
23
+ # Metode ini akan dipanggil untuk mengisi ':<%= uploader.name %>_url'
24
+ def with_<%= uploader.name %>_url(model)
25
+ return nil unless model.<%= uploader.name %>.attached?
20
26
 
21
- def build_assets(assets)
22
- assets.to_a.map { |asset| Rails.application.routes.url_helpers.polymorphic_url(asset) rescue '' }.compact
27
+ # Anda bisa mengubah logika ini, misal menggunakan Rails.application.routes.url_helpers
28
+ # jika Anda membutuhkan URL yang absolut.
29
+ model.<%= uploader.name %>.url
30
+ rescue StandardError
31
+ nil
23
32
  end
24
- <% end -%>
33
+
34
+ <%- else -%>
35
+ # Metode ini akan dipanggil untuk mengisi ':<%= uploader.name %>_urls'
36
+ def with_<%= uploader.name %>_urls(model)
37
+ return [] unless model.<%= uploader.name %>.attached?
38
+
39
+ model.<%= uploader.name %>.map do |attachment|
40
+ # Anda bisa mengubah logika ini juga
41
+ attachment.url
42
+ rescue StandardError
43
+ nil
44
+ end.compact
45
+ end
46
+
47
+ <%- end -%>
48
+ <%- end -%>
49
+
25
50
  end
51
+
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe <%= domain_class_name %>::Builders::<%= @subject_class %>, type: :builder do
6
+ describe '#build' do
7
+ let(:<%= @variable_subject %>) do
8
+ ClassStubber::Model.new(
9
+ 'id' => 'test-id-123',
10
+ <%
11
+ # Get uploader field names to exclude from entity_db_fields
12
+ uploader_names = @uploaders.map { |u| u.name.to_s }
13
+ -%>
14
+ <% @entity_db_fields.each do |field| -%>
15
+ <% next if uploader_names.include?(field.to_s) -%>
16
+ <% column_meta = get_column_meta(field) -%>
17
+ <% case column_meta[:type].to_s -%>
18
+ <% when 'datetime', 'timestamp' -%>
19
+ '<%= field %>' => Time.current,
20
+ <% when 'date' -%>
21
+ '<%= field %>' => Date.current,
22
+ <% when 'time' -%>
23
+ '<%= field %>' => Time.current,
24
+ <% when 'boolean' -%>
25
+ '<%= field %>' => true,
26
+ <% when 'integer', 'bigint' -%>
27
+ '<%= field %>' => 123,
28
+ <% when 'decimal', 'float' -%>
29
+ '<%= field %>' => 123.45,
30
+ <% else -%>
31
+ '<%= field %>' => '<%= field %>_value',
32
+ <% end -%>
33
+ <% end -%>
34
+ <% @uploaders.each do |uploader| -%>
35
+ <% if uploader.type == 'single' -%>
36
+ '<%= uploader.name %>' => ClassStubber::ActiveStorageAttachment.new_single('http://example.com/<%= uploader.name %>.jpg'),
37
+ <% else -%>
38
+ '<%= uploader.name %>' => ClassStubber::ActiveStorageAttachment.new_multiple([
39
+ 'http://example.com/<%= uploader.name %>_1.jpg',
40
+ 'http://example.com/<%= uploader.name %>_2.jpg'
41
+ ]),
42
+ <% end -%>
43
+ <% end -%>
44
+ 'created_at' => Time.current,
45
+ 'updated_at' => Time.current
46
+ )
47
+ end
48
+
49
+ let(:builder) { described_class.new(<%= @variable_subject %>) }
50
+
51
+ it 'builds entity from model' do
52
+ entity = builder.build
53
+
54
+ expect(entity).to be_a(<%= domain_class_name %>::Entities::<%= @subject_class %>)
55
+ expect(entity.id).to eq('test-id-123')
56
+ <% @entity_db_fields.first(2).each do |field| -%>
57
+ <% column_meta = get_column_meta(field) -%>
58
+ <% case column_meta[:type].to_s -%>
59
+ <% when 'datetime', 'timestamp', 'time' -%>
60
+ expect(entity.<%= field %>).to be_a(Time)
61
+ <% when 'date' -%>
62
+ expect(entity.<%= field %>).to be_a(Date)
63
+ <% when 'boolean' -%>
64
+ expect(entity.<%= field %>).to eq(true)
65
+ <% when 'integer', 'bigint' -%>
66
+ expect(entity.<%= field %>).to eq(123)
67
+ <% when 'decimal', 'float' -%>
68
+ expect(entity.<%= field %>).to eq(123.45)
69
+ <% else -%>
70
+ expect(entity.<%= field %>).to eq('<%= field %>_value')
71
+ <% end -%>
72
+ <% end -%>
73
+ end
74
+
75
+ it 'includes all required entity attributes', :aggregate_failures do
76
+ entity = builder.build
77
+
78
+ # Entity must have these attributes (keys must exist, values can be nil for optional)
79
+ expect(entity).to respond_to(:id)
80
+ <% @entity_db_fields.each do |field| -%>
81
+ <% next if uploader_names.include?(field.to_s) -%>
82
+ expect(entity).to respond_to(:<%= field %>)
83
+ <% end -%>
84
+ <% @uploaders.each do |uploader| -%>
85
+ <% if uploader.type == 'single' -%>
86
+ expect(entity).to respond_to(:<%= uploader.name %>_url)
87
+ <% else -%>
88
+ expect(entity).to respond_to(:<%= uploader.name %>_urls)
89
+ <% end -%>
90
+ <% end -%>
91
+ expect(entity).to respond_to(:created_at)
92
+ expect(entity).to respond_to(:updated_at)
93
+ end
94
+ <% if @uploaders.present? -%>
95
+
96
+ describe '#attributes_for_entity' do
97
+ it 'returns hash with uploader attributes', :aggregate_failures do
98
+ attributes = builder.send(:attributes_for_entity)
99
+
100
+ expect(attributes).to be_a(Hash)
101
+ <% @uploaders.each do |uploader| -%>
102
+ <% if uploader.type == 'single' -%>
103
+ expect(attributes).to have_key(:<%= uploader.name %>_url)
104
+ <% else -%>
105
+ expect(attributes).to have_key(:<%= uploader.name %>_urls)
106
+ <% end -%>
107
+ <% end -%>
108
+ end
109
+
110
+ it 'generates correct URL attributes', :aggregate_failures do
111
+ attributes = builder.send(:attributes_for_entity)
112
+
113
+ <% @uploaders.each do |uploader| -%>
114
+ <% if uploader.type == 'single' -%>
115
+ expect(attributes[:<%= uploader.name %>_url]).to eq('http://example.com/<%= uploader.name %>.jpg')
116
+ <% else -%>
117
+ expect(attributes[:<%= uploader.name %>_urls]).to be_an(Array)
118
+ expect(attributes[:<%= uploader.name %>_urls].size).to eq(2)
119
+ <% end -%>
120
+ <% end -%>
121
+ end
122
+ end
123
+ <% end -%>
124
+
125
+ it 'ensures all entity attributes have correct keys and types', :aggregate_failures do
126
+ entity = builder.build
127
+ entity_hash = entity.to_h
128
+
129
+ # All entity attributes must have keys in the hash (source of truth from entity)
130
+ expect(entity_hash).to have_key(:id)
131
+ expect(entity.id).to be_a(String)
132
+ <% @entity_db_fields.each do |field| -%>
133
+ <% next if uploader_names.include?(field.to_s) -%>
134
+ <% column_meta = get_column_meta(field) -%>
135
+
136
+ expect(entity_hash).to have_key(:<%= field %>)
137
+ <% case column_meta[:type].to_s -%>
138
+ <% when 'datetime', 'timestamp', 'time' -%>
139
+ expect(entity.<%= field %>).to be_a(Time) if entity.<%= field %>.present?
140
+ <% when 'date' -%>
141
+ expect(entity.<%= field %>).to be_a(Date) if entity.<%= field %>.present?
142
+ <% when 'boolean' -%>
143
+ expect([TrueClass, FalseClass, NilClass]).to include(entity.<%= field %>.class)
144
+ <% when 'integer', 'bigint' -%>
145
+ expect(entity.<%= field %>).to be_a(Integer) if entity.<%= field %>.present?
146
+ <% when 'decimal', 'float' -%>
147
+ expect(entity.<%= field %>).to be_a(Numeric) if entity.<%= field %>.present?
148
+ <% else -%>
149
+ expect(entity.<%= field %>).to be_a(String).or be_nil
150
+ <% end -%>
151
+ <% end -%>
152
+ <% @uploaders.each do |uploader| -%>
153
+ <% if uploader.type == 'single' -%>
154
+
155
+ expect(entity_hash).to have_key(:<%= uploader.name %>_url)
156
+ expect(entity.<%= uploader.name %>_url).to be_a(String).or be_nil
157
+ <% else -%>
158
+
159
+ expect(entity_hash).to have_key(:<%= uploader.name %>_urls)
160
+ expect(entity.<%= uploader.name %>_urls).to be_a(Array)
161
+ <% end -%>
162
+ <% end -%>
163
+
164
+ expect(entity_hash).to have_key(:created_at)
165
+ expect(entity.created_at).to be_a(Time) if entity.created_at.present?
166
+
167
+ expect(entity_hash).to have_key(:updated_at)
168
+ expect(entity.updated_at).to be_a(Time) if entity.updated_at.present?
169
+ end
170
+
171
+ it 'validates entity type schema definitions', :aggregate_failures do
172
+ # Verify entity has correct Dry::Types definitions
173
+ schema = <%= domain_class_name %>::Entities::<%= @subject_class %>.schema
174
+
175
+ # ID must be required (not optional)
176
+ id_key = schema.key(:id)
177
+ expect(id_key.required?).to be true
178
+ # Check if type is not nilable (Constrained means not optional)
179
+ expect(id_key.type.to_s).to match(/Constrained|Strict/)
180
+ <% @entity_db_fields.each do |field| -%>
181
+ <% next if uploader_names.include?(field.to_s) -%>
182
+ <% column_meta = get_column_meta(field) -%>
183
+
184
+ # <%= field %>: <%= column_meta[:null] ? 'optional' : 'required' %>
185
+ <% if column_meta[:null] -%>
186
+ # Optional field - can be nil
187
+ <%= field %>_key = schema.key(:<%= field %>)
188
+ expect(<%= field %>_key.required?).to be false
189
+ <% else -%>
190
+ # Required field - not nilable
191
+ <%= field %>_key = schema.key(:<%= field %>)
192
+ expect(<%= field %>_key.required?).to be true
193
+ expect(<%= field %>_key.type.to_s).to match(/Constrained|Strict/)
194
+ <% end -%>
195
+ <% end -%>
196
+ <% @uploaders.each do |uploader| -%>
197
+ <% if uploader.type == 'single' -%>
198
+
199
+ # <%= uploader.name %>_url: optional (uploader can be nil)
200
+ <%= uploader.name %>_url_key = schema.key(:<%= uploader.name %>_url)
201
+ expect(<%= uploader.name %>_url_key.required?).to be false
202
+ <% else -%>
203
+
204
+ # <%= uploader.name %>_urls: required (but can be empty array)
205
+ <%= uploader.name %>_urls_key = schema.key(:<%= uploader.name %>_urls)
206
+ expect(<%= uploader.name %>_urls_key.required?).to be true
207
+ <% end -%>
208
+ <% end -%>
209
+
210
+ # Timestamps - check their actual definition
211
+ created_at_key = schema.key(:created_at)
212
+ updated_at_key = schema.key(:updated_at)
213
+
214
+ # Verify timestamps exist (they might be optional in some entities)
215
+ expect(schema.keys.map(&:name)).to include(:created_at, :updated_at)
216
+ end
217
+ end
218
+ end
219
+
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Core::Builders::Error < RiderKick::Builders::AbstractActiveRecordEntityBuilder
4
- acts_as_builder_for_entity Core::Entities::Error
3
+ class <%= domain_class_name %>::Builders::Error < RiderKick::Builders::AbstractActiveRecordEntityBuilder
4
+ acts_as_builder_for_entity <%= domain_class_name %>::Entities::Error
5
5
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Core::Builders::Pagination < RiderKick::Builders::AbstractActiveRecordEntityBuilder
4
- acts_as_builder_for_entity Core::Entities::Pagination
3
+ class <%= domain_class_name %>::Builders::Pagination < RiderKick::Builders::AbstractActiveRecordEntityBuilder
4
+ acts_as_builder_for_entity <%= domain_class_name %>::Entities::Pagination
5
5
 
6
6
  def attributes_for_entity
7
7
  {
@@ -1,17 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Core::Entities::<%= @subject_class %> < Dry::Struct
4
- <% (['id'] + @fields + @uploaders + ['created_at', 'updated_at']).each do |field| -%>
5
- <% column_type = get_column_type(field) -%>
6
- <% dry_type = @entity_type_mapping[column_type.to_s] || 'Types::Strict::String' -%>
7
- <% if @uploaders.include?(field) -%>
8
- <% if is_singular?(field) -%>
9
- attribute :<%= field %>, Types::Strict::String
10
- <% else -%>
11
- attribute :<%= field %>, Types::Strict::Array
12
- <% end -%>
13
- <% else -%>
14
- attribute :<%= field %>, <%= dry_type %>
15
- <% end -%>
16
- <% end -%>
3
+ class <%= domain_class_name %>::Entities::<%= @subject_class %> < Dry::Struct
4
+ transform_keys(&:to_sym)
5
+
6
+ # Atribut Wajib (selalu ada)
7
+ attribute :id, Types::Strict::String
8
+ <%- @entity_db_fields.reject { |f| f == 'id' }.each do |field| -%>
9
+ <%-
10
+ # Dapatkan tipe data dari pemetaan
11
+ db_type = get_column_type(field).to_s
12
+ entity_type = @entity_type_mapping[db_type] || 'Types::Strict::String' # Fallback
13
+ # Cek nullability dari @columns_meta_hash, BUKAN @model_class.columns_hash
14
+ column_meta = get_column_meta(field) # Menggunakan helper baru
15
+ is_nullable = column_meta[:null] == true
16
+
17
+ # Terapkan .optional jika nullable di DB
18
+ entity_type += ".optional" if is_nullable
19
+ -%>
20
+ attribute :<%= field %>, <%= entity_type %>
21
+ <%- end -%>
22
+
23
+ # Atribut ini diambil dari 'uploaders' di YAML
24
+ <%- @uploaders.each do |uploader| -%>
25
+ <%- if uploader.type == 'single' -%>
26
+ attribute :<%= uploader.name %>_url, Types::Strict::String.optional
27
+ <%- else -%>
28
+ attribute :<%= uploader.name %>_urls, Types::Strict::Array.of(Types::Strict::String).optional
29
+ <%- end -%>
30
+ <%- end -%>
31
+
32
+ # Atribut Opsional (selalu ada)
33
+ attribute? :created_at, Types::Strict::Time.optional
34
+ attribute? :updated_at, Types::Strict::Time.optional
17
35
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Core::Entities::Error < Dry::Struct
3
+ class <%= domain_class_name %>::Entities::Error < Dry::Struct
4
4
  attribute :attribute, Types::Strict::String
5
5
  attribute :type, Types::Strict::String
6
6
  attribute :options, Types::Hash.optional
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Core::Entities::Pagination < Dry::Struct
3
+ class <%= domain_class_name %>::Entities::Pagination < Dry::Struct
4
4
  attribute :total_count, Types::Strict::Integer
5
5
  attribute :page, Types::Strict::Integer
6
6
  attribute :per_page, Types::Strict::Integer