rider-kick 0.0.13 → 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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +629 -25
  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 -45
  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 +22 -13
  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 +95 -22
  13. data/lib/generators/rider_kick/scaffold_generator.rb +377 -62
  14. data/lib/generators/rider_kick/scaffold_generator_builder_uploaders_spec.rb +119 -14
  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 +37 -10
  17. data/lib/generators/rider_kick/scaffold_generator_contracts_with_scope_spec.rb +40 -11
  18. data/lib/generators/rider_kick/scaffold_generator_engine_spec.rb +221 -0
  19. data/lib/generators/rider_kick/scaffold_generator_idempotent_spec.rb +38 -13
  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 +31 -12
  23. data/lib/generators/rider_kick/scaffold_generator_with_scope_spec.rb +32 -11
  24. data/lib/generators/rider_kick/structure_generator.rb +154 -43
  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 +3 -3
  28. data/lib/generators/rider_kick/structure_generator_success_spec.rb +33 -5
  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 +140 -66
  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 -4
  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 +13 -8
  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 +3 -3
  66. data/lib/generators/rider_kick/templates/domains/core/utils/request_methods.rb.tt +1 -1
  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 +8 -6
  79. data/lib/rider_kick/builders/abstract_active_record_entity_builder_spec.rb +644 -0
  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 +1 -1
  83. data/lib/rider_kick/entities/failure_details_spec.rb +1 -1
  84. data/lib/rider_kick/matchers/use_case_result.rb +1 -1
  85. data/lib/rider_kick/use_cases/abstract_use_case.rb +1 -1
  86. data/lib/rider_kick/version.rb +1 -1
  87. metadata +129 -8
  88. data/CHANGELOG.md +0 -5
@@ -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,7 +1,13 @@
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
5
11
 
6
12
  fields:
7
13
  <% @fields.each do |f| -%>
@@ -11,122 +17,190 @@ fields:
11
17
  - <%= f %>
12
18
  <% end -%>
13
19
 
20
+ <% if entity_uploader_definitions.empty? -%>
21
+ uploaders: []
22
+ <% else -%>
14
23
  uploaders:
15
- <% @uploaders.each do |f| -%>
16
- - <%= f %>
24
+ <% entity_uploader_definitions.each do |uploader_hash| -%>
25
+ - { name: '<%= uploader_hash[:name] %>', type: '<%= uploader_hash[:type] %>' }
26
+ <% end -%>
17
27
  <% end -%>
18
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 -%>
19
33
  search_able:
20
- <% @search_able.each do |f| -%>
34
+ <% search_able_fields.each do |f| -%>
21
35
  - <%= f %>
22
36
  <% end -%>
37
+ <% end -%>
23
38
 
24
39
  # ---- Enriched metadata (opsional, untuk tooling/insight) ----
25
40
  schema:
26
- columns:
41
+ columns:
42
+ <% if columns_meta.empty? -%>
43
+ []
44
+ <% else -%>
27
45
  <% columns_meta.each do |c| -%>
28
- - name: <%= c[:name] %>
29
- type: <%= c[:type] %>
30
- sql_type: <%= c[:sql_type] %>
31
- null: <%= c[:null] %>
46
+ - name: <%= c[:name] %>
47
+ type: <%= c[:type] %>
48
+ sql_type: <%= c[:sql_type] || 'null' %>
49
+ null: <%= c[:null] || 'false' %>
32
50
  <% if c[:default].present? -%>
33
- default: <%= c[:default].inspect %>
51
+ default: <%= c[:default].inspect %>
34
52
  <% end -%>
35
53
  <% if c[:precision] || c[:scale] -%>
36
- precision: <%= c[:precision] || 'null' %>
37
- scale: <%= c[:scale] || 'null' %>
54
+ precision: <%= c[:precision] || 'null' %>
55
+ scale: <%= c[:scale] || 'null' %>
38
56
  <% end -%>
39
57
  <% if c[:limit] -%>
40
- limit: <%= c[:limit] %>
58
+ limit: <%= c[:limit] %>
41
59
  <% end -%>
42
60
  <% end -%>
43
- foreign_keys:
61
+ <% end -%>
62
+ foreign_keys:
44
63
  <% (fkeys_meta.presence || []).each do |fk| -%>
45
- - column: <%= fk[:column] %>
46
- to_table: <%= fk[:to_table] %>
64
+ - column: <%= fk[:column] %>
65
+ to_table: <%= fk[:to_table] %>
66
+ <% end -%>
67
+ <% if fkeys_meta.empty? -%>
68
+ []
47
69
  <% end -%>
48
- indexes:
70
+ indexes:
49
71
  <% (indexes_meta.presence || []).each do |ix| -%>
50
- - columns: [<%= ix[:columns].join(', ') %>]
51
- unique: <%= ix[:unique] %>
72
+ - columns: [<%= ix[:columns].join(', ') %>]
73
+ unique: <%= ix[:unique] %>
52
74
  <% end -%>
53
- enums:
75
+ <% if indexes_meta.empty? -%>
76
+ []
77
+ <% end -%>
78
+ enums:
54
79
  <% if enums_meta.present? -%>
55
80
  <% enums_meta.each do |name, map| -%>
56
- <%= name %>: <%= map.keys %>
81
+ <%= name %>: <%= map.keys %>
57
82
  <% end -%>
58
83
  <% else -%>
59
- {}
84
+ {}
60
85
  <% end -%>
61
86
 
62
87
  controllers:
63
- list_fields:
88
+ list_fields:
64
89
  <% @fields.each do |f| -%>
65
- - <%= f %>
90
+ - <%= f %>
66
91
  <% end -%>
67
- show_fields:
68
- <% (@columns.map { _1[:name] } + @uploaders).uniq.each do |f| -%>
69
- - <%= f %>
92
+ <% if @fields.empty? -%>
93
+ []
94
+ <% end -%>
95
+ show_fields:
96
+ <% (@columns.map { |c| c[:name] } + @uploaders).uniq.each do |f| -%>
97
+ - <%= f %>
70
98
  <% end -%>
71
- form_fields:
99
+ <% if (@columns.map { |c| c[:name] } + @uploaders).uniq.empty? -%>
100
+ []
101
+ <% end -%>
102
+ form_fields:
103
+ <% if @fields.empty? && @uploaders.empty? -%>
104
+ []
105
+ <% else -%>
72
106
  <% @fields.each do |f| -%>
73
- - name: <%= f %>
74
- type: <%= get_column_type(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 -%>
75
114
  <% end -%>
76
115
  <% @uploaders.each do |f| -%>
77
- - name: <%= f %>
78
- type: <%= is_singular?(f) ? 'file' : 'files' %>
116
+ <% unless @fields.include?(f) -%>
117
+ - name: <%= f %>
118
+ type: <%= is_singular?(f) ? 'file' : 'files' %>
119
+ <% end -%>
120
+ <% end -%>
79
121
  <% end -%>
80
122
 
81
123
  domains:
82
- action_list:
83
- use_case:
84
- contract:
85
- <% @search_able.each do |f| -%>
86
- # optional search fields: <%= f %>
87
- <% end -%>
88
- action_fetch_by_id:
89
- use_case:
90
- contract:
91
- - required(:id).filled(:string)
124
+ action_list:
125
+ use_case:
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
+
144
+ action_fetch_by_id:
145
+ use_case:
146
+ contract:
147
+ - "required(:id).filled(:string)"
92
148
  <% if @resource_owner_id.present? -%>
93
- - required(:<%= @resource_owner_id %>).filled(:string)
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 -%>
94
156
  <% end -%>
95
157
 
96
- action_create:
97
- use_case:
98
- contract:
158
+ action_create:
159
+ use_case:
160
+ contract:
99
161
  <% if @resource_owner_id.present? -%>
100
- - required(:<%= @resource_owner_id %>).filled(:string)
162
+ - "required(:<%= @resource_owner_id %>).filled(:string)"
101
163
  <% end -%>
164
+ <% if contract_lines_for_create.empty? && !@resource_owner_id.present? -%>
165
+ []
166
+ <% else -%>
102
167
  <% contract_lines_for_create.each do |line| -%>
103
- <%= line %>
168
+ - <%= line %>
169
+ <% end -%>
104
170
  <% end -%>
105
171
 
106
- action_update:
107
- use_case:
108
- contract:
109
- - required(:id).filled(:string)
172
+ action_update:
173
+ use_case:
174
+ contract:
175
+ - "required(:id).filled(:string)"
110
176
  <% if @resource_owner_id.present? -%>
111
- - required(:<%= @resource_owner_id %>).filled(:string)
177
+ - "required(:<%= @resource_owner_id %>).filled(:string)"
112
178
  <% end -%>
113
179
  <% contract_lines_for_update.each do |line| -%>
114
- <%= line %>
180
+ - <%= line %>
115
181
  <% end -%>
116
182
 
117
- action_destroy:
118
- use_case:
119
- contract:
120
- - required(:id).filled(:string)
183
+ action_destroy:
184
+ use_case:
185
+ contract:
186
+ - "required(:id).filled(:string)"
121
187
  <% if @resource_owner_id.present? -%>
122
- - required(:<%= @resource_owner_id %>).filled(:string)
188
+ - "required(:<%= @resource_owner_id %>).filled(:string)"
123
189
  <% end -%>
124
190
 
125
191
  entity:
126
- skipped_fields:
127
- - id
128
- - created_at
129
- - updated_at
130
- <% if @columns.map { _1[:name] }.include?('type') -%>
131
- - type
192
+ skipped_fields:
193
+ - id
194
+ - created_at
195
+ - updated_at
196
+ <% if @columns.map { |c| c[:name] }.include?('type') -%>
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 %>
132
205
  <% end -%>
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