sun-sword 0.0.11 → 0.0.12

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -2
  3. data/Gemfile +8 -0
  4. data/Gemfile.lock +114 -55
  5. data/README.md +35 -13
  6. data/Rakefile +6 -10
  7. data/lib/generators/sun_sword/USAGE +22 -3
  8. data/lib/generators/sun_sword/frontend_generator.rb +77 -39
  9. data/lib/generators/sun_sword/frontend_generator_spec.rb +539 -0
  10. data/lib/generators/sun_sword/init_generator.rb +2 -0
  11. data/lib/generators/sun_sword/init_generator_spec.rb +82 -0
  12. data/lib/generators/sun_sword/scaffold_generator.rb +189 -24
  13. data/lib/generators/sun_sword/scaffold_generator_spec.rb +1414 -0
  14. data/lib/generators/sun_sword/templates_frontend/{Procfile.dev → Procfile.dev.tt} +1 -0
  15. data/lib/generators/sun_sword/templates_frontend/bin/{watch → watch.tt} +2 -1
  16. data/lib/generators/sun_sword/templates_frontend/config/{vite.json → vite.json.tt} +2 -1
  17. data/lib/generators/sun_sword/templates_frontend/controllers/application_controller.rb.tt +2 -2
  18. data/lib/generators/sun_sword/templates_frontend/controllers/tests_controller.rb +36 -0
  19. data/lib/generators/sun_sword/templates_frontend/controllers/tests_controller_spec.rb +62 -0
  20. data/lib/generators/sun_sword/templates_frontend/env.development +1 -1
  21. data/lib/generators/sun_sword/templates_frontend/frontend/entrypoints/application.js +2 -2
  22. data/lib/generators/sun_sword/templates_frontend/frontend/pages/tests-stimulus.js +31 -0
  23. data/lib/generators/sun_sword/templates_frontend/frontend/pages/web.js +12 -0
  24. data/lib/generators/sun_sword/templates_frontend/frontend/stylesheets/application.css +1 -3
  25. data/lib/generators/sun_sword/templates_frontend/helpers/application_helper.rb +17 -0
  26. data/lib/generators/sun_sword/templates_frontend/helpers/application_helper_spec.rb +30 -0
  27. data/lib/generators/sun_sword/templates_frontend/package.json.tt +14 -0
  28. data/lib/generators/sun_sword/templates_frontend/views/components/_action_destroy.html.erb.tt +1 -1
  29. data/lib/generators/sun_sword/templates_frontend/views/components/_alert.html.erb.tt +1 -1
  30. data/lib/generators/sun_sword/templates_frontend/views/layouts/application.html.erb.tt +3 -3
  31. data/lib/generators/sun_sword/templates_frontend/views/tests/_frame_content.html.erb +9 -0
  32. data/lib/generators/sun_sword/templates_frontend/views/tests/_log_entry.html.erb +4 -0
  33. data/lib/generators/sun_sword/templates_frontend/views/tests/_updated_content.html.erb +6 -0
  34. data/lib/generators/sun_sword/templates_frontend/views/tests/stimulus.html.erb +45 -0
  35. data/lib/generators/sun_sword/templates_frontend/views/tests/turbo_drive.html.erb +55 -0
  36. data/lib/generators/sun_sword/templates_frontend/views/tests/turbo_frame.html.erb +87 -0
  37. data/lib/generators/sun_sword/templates_frontend/vite.config.ts.tt +1 -1
  38. data/lib/generators/sun_sword/templates_init/config/initializers/sun_sword.rb +1 -0
  39. data/lib/generators/sun_sword/templates_scaffold/controllers/controller.rb.tt +24 -24
  40. data/lib/generators/sun_sword/templates_scaffold/controllers/controller_spec.rb.tt +398 -0
  41. data/lib/generators/sun_sword/templates_scaffold/views/index.html.erb.tt +5 -5
  42. data/lib/generators/sun_sword/templates_scaffold/views/show.html.erb.tt +3 -0
  43. data/lib/generators/tmp/db/structures/test_structure.yaml +42 -0
  44. data/lib/sun-sword.rb +1 -0
  45. data/lib/sun_sword/configuration_spec.rb +77 -0
  46. data/lib/sun_sword/version.rb +1 -1
  47. metadata +84 -30
  48. data/lib/generators/sun_sword/templates_frontend/controllers/site_controller.rb +0 -16
  49. data/lib/generators/sun_sword/templates_frontend/db/seeds.rb +0 -3
  50. data/lib/generators/sun_sword/templates_frontend/db/structures/example.yaml.tt +0 -106
  51. data/lib/generators/sun_sword/templates_frontend/frontend/pages/stimulus.js +0 -10
  52. data/lib/generators/sun_sword/templates_frontend/package.json +0 -7
  53. data/lib/generators/sun_sword/templates_frontend/views/site/_comment.html.erb.tt +0 -3
  54. data/lib/generators/sun_sword/templates_frontend/views/site/stimulus.html.erb.tt +0 -26
@@ -1,17 +1,17 @@
1
1
  # Template for the controller (controllers/controller.rb.tt)
2
- class <%= [@route_scope_class, @scope_class].reject { |c| c.empty? }.join("::") %>Controller < ApplicationController
2
+ class <%= [@engine_scope_class, @scope_class].reject { |c| c.empty? }.join("::") %>Controller < <%= [@engine_scope_class].reject(&:empty?).join("::").sub(/(.+)/, '\1::') %>ApplicationController
3
3
  before_action :set_<%= @variable_subject %>, only: %i[edit update]
4
4
  layout :set_layouts
5
5
 
6
- # GET /<%= [@route_scope_path, @scope_path].reject { |c| c.empty? }.join("/") %>
6
+ # GET /<engine_mount_path>/<%=[@scope_path].reject { |c| c.empty? }.join("/") %>
7
7
  def index
8
- use_case = Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('list')].reject { |c| c.empty? }.join("::") %>
8
+ use_case = <%= [@engine_structure_class].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@scope_class, build_usecase_filename('list')].reject { |c| c.empty? }.join("::") %>
9
9
  contract = use_case.contract!(build_contract({}))
10
10
  result = use_case.new(contract).result
11
11
  Dry::Matcher::ResultMatcher.call(result) do |matcher|
12
12
  matcher.success do |response|
13
13
  @<%= @variable_subject.pluralize %> = response
14
- render '<%= @route_scope_path %>/<%= @scope_path %>/index'
14
+ render '<%= [@engine_mount_path, @scope_path, "index"].reject { |c| c.empty? }.join("/") %>'
15
15
  end
16
16
  matcher.failure do |errors|
17
17
  redirect_to root_path, success: errors
@@ -19,9 +19,9 @@ class <%= [@route_scope_class, @scope_class].reject { |c| c.empty? }.join("::")
19
19
  end
20
20
  end
21
21
 
22
- # GET /<%= [@route_scope_path, @scope_path, ":uuid"].reject { |c| c.empty? }.join("/") %>
22
+ # GET /<engine_mount_path>/<%=[@scope_path, ":uuid"].reject { |c| c.empty? }.join("/") %>
23
23
  def show
24
- use_case = Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>
24
+ use_case = <%= [@engine_structure_class].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>
25
25
  contract = use_case.contract!(build_contract({ id: params[:id] }))
26
26
  result = use_case.new(contract).result
27
27
  Dry::Matcher::ResultMatcher.call(result) do |matcher|
@@ -34,69 +34,69 @@ class <%= [@route_scope_class, @scope_class].reject { |c| c.empty? }.join("::")
34
34
  end
35
35
  end
36
36
 
37
- # GET /<%= [@route_scope_path, @scope_path, "new"].reject { |c| c.empty? }.join("/") %>
37
+ # GET /<engine_mount_path>/<%=[@scope_path, "new"].reject { |c| c.empty? }.join("/") %>
38
38
  def new
39
39
  @<%= @variable_subject %> = <%= @model_class %>.new
40
40
  end
41
41
 
42
- # GET /<%= [@route_scope_path, @scope_path, ":uuid", "edit"].reject { |c| c.empty? }.join("/") %>
42
+ # GET /<engine_mount_path>/<%=[@scope_path, ":uuid", "edit"].reject { |c| c.empty? }.join("/") %>
43
43
  def edit
44
44
  end
45
45
 
46
- # POST /<%= @route_scope_path %>/<%= @scope_path %>
46
+ # POST /<%= @engine_scope_path %>/<%= @scope_path %>
47
47
  def create
48
- use_case = Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('create')].reject { |c| c.empty? }.join("::") %>
48
+ use_case = <%= [@engine_structure_class].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@scope_class, build_usecase_filename('create')].reject { |c| c.empty? }.join("::") %>
49
49
  contract = use_case.contract!(build_contract(<%= @variable_subject %>_params))
50
50
  result = use_case.new(contract).result
51
51
  Dry::Matcher::ResultMatcher.call(result) do |matcher|
52
52
  matcher.success do |response|
53
- redirect_to <%= [@route_scope_path, @scope_path.singularize].reject { |c| c.empty? }.join("_") %>_url(id: response.id), success: '<%= @subject_class %> was successfully created.'
53
+ redirect_to <%= [@scope_path.singularize].reject { |c| c.empty? }.join("_") %>_url(id: response.id), success: '<%= @subject_class %> was successfully created.'
54
54
  end
55
55
  matcher.failure do |errors|
56
56
  @<%= @variable_subject %> = build_form_errors(<%= @variable_subject %>_params, <%= @model_class %>.new, errors)
57
- render '<%= [@route_scope_path, @scope_path, "new"].reject { |c| c.empty? }.join("/") %>', status: :unprocessable_entity
57
+ render '<%= [@engine_mount_path, @scope_path, "new"].reject { |c| c.empty? }.join("/") %>', status: :unprocessable_entity
58
58
  end
59
59
  end
60
60
  end
61
61
 
62
- # PATCH/PUT /<%= [@route_scope_path, @scope_path, ":uuid"].reject { |c| c.empty? }.join("/") %>
62
+ # PATCH/PUT /<engine_mount_path>/<%=[@scope_path, ":uuid"].reject { |c| c.empty? }.join("/") %>
63
63
  def update
64
- use_case = Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('update')].reject { |c| c.empty? }.join("::") %>
64
+ use_case = <%= [@engine_structure_class].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@scope_class, build_usecase_filename('update')].reject { |c| c.empty? }.join("::") %>
65
65
  contract = use_case.contract!(build_contract(<%= @variable_subject %>_params).merge({ id: params[:id] }))
66
66
  result = use_case.new(contract).result
67
67
  Dry::Matcher::ResultMatcher.call(result) do |matcher|
68
68
  matcher.success do |response|
69
- redirect_to <%= [@route_scope_path, @scope_path.singularize].reject { |c| c.empty? }.join("_") %>_url(id: response.id), success: '<%= @subject_class %> was successfully updated.'
69
+ redirect_to <%= [@scope_path.singularize].reject { |c| c.empty? }.join("_") %>_url(id: response.id), success: '<%= @subject_class %> was successfully updated.'
70
70
  end
71
71
  matcher.failure do |errors|
72
72
  @<%= @variable_subject %> = build_form_errors(<%= @variable_subject %>_params, <%= @model_class %>.find(params[:id]), errors)
73
- render '<%= [@route_scope_path, @scope_path, "edit"].reject { |c| c.empty? }.join("/") %>', status: :unprocessable_entity
73
+ render '<%= [@engine_mount_path, @scope_path, "edit"].reject { |c| c.empty? }.join("/") %>', status: :unprocessable_entity
74
74
  end
75
75
  end
76
76
  end
77
77
 
78
- # DELETE /<%= [@route_scope_path, @scope_path, ":uuid"].reject { |c| c.empty? }.join("/") %>
78
+ # DELETE /<engine_mount_path>/<%=[@scope_path, ":uuid"].reject { |c| c.empty? }.join("/") %>
79
79
  def destroy
80
- use_case = Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('destroy')].reject { |c| c.empty? }.join("::") %>
80
+ use_case = <%= [@engine_structure_class].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@scope_class, build_usecase_filename('destroy')].reject { |c| c.empty? }.join("::") %>
81
81
  contract = use_case.contract!(build_contract({ id: params[:id] }))
82
82
  result = use_case.new(contract).result
83
83
  Dry::Matcher::ResultMatcher.call(result) do |matcher|
84
84
  matcher.success do |response|
85
- redirect_to <%= [@route_scope_path, @scope_path].reject { |c| c.empty? }.join("_") %>_url, notice: '<%= @subject_class %> was successfully destroyed.'
85
+ redirect_to <%= [@scope_path].reject { |c| c.empty? }.join("_") %>_url, notice: '<%= @subject_class %> was successfully destroyed.'
86
86
  end
87
87
  matcher.failure do |errors|
88
- redirect_to <%= [@route_scope_path, @scope_path].reject { |c| c.empty? }.join("_") %>_url, error: '<%= @subject_class %> could not be destroyed.'
88
+ redirect_to <%= [@scope_path].reject { |c| c.empty? }.join("_") %>_url, error: '<%= @subject_class %> could not be destroyed.'
89
89
  end
90
90
  end
91
91
  end
92
92
 
93
93
  private
94
94
 
95
- def build_contract(params)
95
+ def build_contract(args)
96
96
  <%if @resource_owner_id.present? -%>
97
- { <%=@resource_owner_id%>: <%=@resource_owner_id%> }.merge(params)
97
+ { <%=@resource_owner_id%>: <%=@resource_owner_id%> }.merge(args).to_h
98
98
  <%else -%>
99
- {}.merge(params)
99
+ {}.merge(args).to_h
100
100
  <%end -%>
101
101
  end
102
102
 
@@ -107,7 +107,7 @@ class <%= [@route_scope_class, @scope_class].reject { |c| c.empty? }.join("::")
107
107
  <%else -%>
108
108
  @<%= @variable_subject %> = <%= @model_class %>.find_by(id: params[:id])
109
109
  <%end -%>
110
- redirect_to <%= [@route_scope_path, @scope_path].reject { |c| c.empty? }.join("_") %>_url, error: '<%= @subject_class %> not found.' if @<%= @variable_subject %>.nil?
110
+ redirect_to <%= [@scope_path].reject { |c| c.empty? }.join("_") %>_url, error: '<%= @subject_class %> not found.' if @<%= @variable_subject %>.nil?
111
111
  end
112
112
 
113
113
  # Only allow a list of trusted parameters through.
@@ -0,0 +1,398 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe <%= [@engine_scope_class, @scope_class].reject { |c| c.empty? }.join("::") %>Controller, type: :controller do
6
+ <% if @resource_owner_id.present? -%>
7
+ let(:owner) { create(:<%= @resource_owner %>) }
8
+ let(:actor) { create(:actor) }
9
+ <% end -%>
10
+
11
+ let(:valid_attributes) do
12
+ {
13
+ <% @fields.each_with_index do |(field, type), index| -%>
14
+ <% next if field.to_s == 'id' || field.to_s.end_with?('_id') -%>
15
+ <%= field %>: <%= case type.to_s
16
+ when 'string' then "'Sample #{field}'"
17
+ when 'text' then "'Sample text for #{field}'"
18
+ when 'integer' then '1'
19
+ when 'float', 'decimal' then '1.5'
20
+ when 'boolean' then 'true'
21
+ when 'date' then 'Date.today'
22
+ when 'datetime' then 'Time.current'
23
+ else "'value'"
24
+ end %><%= ',' unless index == @fields.size - 1 %>
25
+ <% end -%>
26
+ }
27
+ end
28
+
29
+ let(:invalid_attributes) do
30
+ {
31
+ <% @fields.first(2).each_with_index do |(field, type), index| -%>
32
+ <% next if field.to_s == 'id' || field.to_s.end_with?('_id') -%>
33
+ <%= field %>: nil<%= ',' unless index == 1 %>
34
+ <% end -%>
35
+ }
36
+ end
37
+
38
+ <% if @resource_owner_id.present? -%>
39
+ before do
40
+ allow(controller).to receive(:<%= @resource_owner_id %>).and_return(owner.id)
41
+ end
42
+
43
+ <% end -%>
44
+ describe 'GET #index' do
45
+ context 'when use case succeeds' do
46
+ let(:<%= @variable_subject.pluralize %>) { create_list(:<%= @variable_subject %>) }
47
+ let(:use_case_instance) { instance_double(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('list')].reject { |c| c.empty? }.join("::") %>) }
48
+ let(:success_result) { Dry::Monads::Success(<%= @variable_subject.pluralize %>) }
49
+
50
+ before do
51
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('list')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!).and_return(true)
52
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('list')].reject { |c| c.empty? }.join("::") %>).to receive(:new).and_return(use_case_instance)
53
+ allow(use_case_instance).to receive(:result).and_return(success_result)
54
+ end
55
+
56
+ it 'returns success response and assigns @<%= @variable_subject.pluralize %>' do
57
+ get :index
58
+
59
+ expect(response).to have_http_status(:success)
60
+ expect(assigns(:<%= @variable_subject.pluralize %>)).to eq(<%= @variable_subject.pluralize %>)
61
+ expect(response).to render_template('<%= [@route_scope_path, @scope_path, 'index'].reject { |c| c.empty? }.join("/") %>')
62
+ end
63
+
64
+ it 'calls use case with correct contract' do
65
+ expect(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('list')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!)<%= @resource_owner_id.present? ? "\n .with(hash_including(#{@resource_owner_id}: owner.id))" : '' %>
66
+
67
+ get :index
68
+ end
69
+ end
70
+
71
+ context 'when use case fails' do
72
+ let(:use_case_instance) { instance_double(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('list')].reject { |c| c.empty? }.join("::") %>) }
73
+ let(:failure_result) { Dry::Monads::Failure('Error loading <%= @variable_subject.pluralize %>') }
74
+
75
+ before do
76
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('list')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!).and_return(true)
77
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('list')].reject { |c| c.empty? }.join("::") %>).to receive(:new).and_return(use_case_instance)
78
+ allow(use_case_instance).to receive(:result).and_return(failure_result)
79
+ end
80
+
81
+ it 'redirects to root path with error' do
82
+ get :index
83
+
84
+ expect(response).to redirect_to(root_path)
85
+ expect(flash[:success]).to eq('Error loading <%= @variable_subject.pluralize %>')
86
+ end
87
+ end
88
+ end
89
+
90
+ describe 'GET #show' do
91
+ let(:<%= @variable_subject %>) { create(:<%= @variable_subject %><%= @resource_owner_id.present? ? ", #{@resource_owner_id}: owner.id" : '' %>) }
92
+
93
+ context 'when use case succeeds' do
94
+ let(:use_case_instance) { instance_double(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>) }
95
+ let(:success_result) { Dry::Monads::Success(<%= @variable_subject %>) }
96
+
97
+ before do
98
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!).and_return(true)
99
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>).to receive(:new).and_return(use_case_instance)
100
+ allow(use_case_instance).to receive(:result).and_return(success_result)
101
+ end
102
+
103
+ it 'returns success response and assigns @<%= @variable_subject %>' do
104
+ get :show, params: { id: <%= @variable_subject %>.id }
105
+
106
+ expect(response).to have_http_status(:success)
107
+ expect(assigns(:<%= @variable_subject %>)).to eq(<%= @variable_subject %>)
108
+ end
109
+
110
+ it 'calls use case with correct contract' do
111
+ expect(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!)
112
+ .with(hash_including(<%= @resource_owner_id.present? ? "#{@resource_owner_id}: owner.id, " : '' %>id: <%= @variable_subject %>.id))
113
+
114
+ get :show, params: { id: <%= @variable_subject %>.id }
115
+ end
116
+ end
117
+
118
+ context 'when use case fails' do
119
+ let(:use_case_instance) { instance_double(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>) }
120
+ let(:failure_result) { Dry::Monads::Failure('<%= @subject_class %> not found') }
121
+
122
+ before do
123
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!).and_return(true)
124
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>).to receive(:new).and_return(use_case_instance)
125
+ allow(use_case_instance).to receive(:result).and_return(failure_result)
126
+ end
127
+
128
+ it 'redirects to root path with error' do
129
+ get :show, params: { id: 'non-existent' }
130
+
131
+ expect(response).to redirect_to(root_path)
132
+ expect(flash[:success]).to eq('<%= @subject_class %> not found')
133
+ end
134
+ end
135
+ end
136
+
137
+ describe 'GET #new' do
138
+ it 'returns success response and assigns new <%= @variable_subject %>' do
139
+ get :new
140
+
141
+ expect(response).to have_http_status(:success)
142
+ expect(assigns(:<%= @variable_subject %>)).to be_a_new(<%= @model_class %>)
143
+ end
144
+ end
145
+
146
+ describe 'GET #edit' do
147
+ let(:<%= @variable_subject %>) { create(:<%= @variable_subject %><%= @resource_owner_id.present? ? ", #{@resource_owner_id}: owner.id" : '' %>) }
148
+
149
+ context 'when <%= @variable_subject %> exists<%= @resource_owner_id.present? ? " and belongs to owner" : '' %>' do
150
+ before do
151
+ allow(<%= @model_class %>).to receive(:find_by)
152
+ .with(<%= @resource_owner_id.present? ? "id: #{@variable_subject}.id, #{@resource_owner_id}: owner.id" : "id: #{@variable_subject}.id" %>)
153
+ .and_return(<%= @variable_subject %>)
154
+ end
155
+
156
+ it 'returns success response and assigns <%= @variable_subject %>' do
157
+ get :edit, params: { id: <%= @variable_subject %>.id }
158
+
159
+ expect(response).to have_http_status(:success)
160
+ expect(assigns(:<%= @variable_subject %>)).to eq(<%= @variable_subject %>)
161
+ end
162
+ end
163
+
164
+ context 'when <%= @variable_subject %> does not exist<%= @resource_owner_id.present? ? " or does not belong to owner" : '' %>' do
165
+ before do
166
+ allow(<%= @model_class %>).to receive(:find_by)
167
+ .with(<%= @resource_owner_id.present? ? "id: 'non-existent', #{@resource_owner_id}: owner.id" : "id: 'non-existent'" %>)
168
+ .and_return(nil)
169
+ end
170
+
171
+ it 'redirects with error' do
172
+ get :edit, params: { id: 'non-existent' }
173
+
174
+ expect(response).to redirect_to(<%= [@route_scope_path, @scope_path].reject { |c| c.empty? }.join("_") %>_url)
175
+ expect(flash[:error]).to eq('<%= @subject_class %> not found.')
176
+ end
177
+ end
178
+ end
179
+
180
+ describe 'POST #create' do
181
+ let(:use_case_instance) { instance_double(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('create')].reject { |c| c.empty? }.join("::") %>) }
182
+
183
+ context 'with valid params' do
184
+ let(:created_<%= @variable_subject %>) { build(:<%= @variable_subject %>, id: 'uuid-123', **valid_attributes) }
185
+ let(:success_result) { Dry::Monads::Success(created_<%= @variable_subject %>) }
186
+
187
+ before do
188
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('create')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!).and_return(true)
189
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('create')].reject { |c| c.empty? }.join("::") %>).to receive(:new).and_return(use_case_instance)
190
+ allow(use_case_instance).to receive(:result).and_return(success_result)
191
+ end
192
+
193
+ it 'creates and redirects with success message' do
194
+ post :create, params: { models_<%= @subject_class.underscore %>: valid_attributes }
195
+
196
+ expect(response).to redirect_to(<%= [@route_scope_path, @scope_path.singularize].reject { |c| c.empty? }.join("_") %>_url(id: created_<%= @variable_subject %>.id))
197
+ expect(flash[:success]).to eq('<%= @subject_class %> was successfully created.')
198
+ end
199
+
200
+ it 'calls use case with correct contract' do
201
+ expect(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('create')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!)
202
+ .with(hash_including(<%= @resource_owner_id.present? ? "#{@resource_owner_id}: owner.id, " : '' %>**valid_attributes.stringify_keys))
203
+
204
+ post :create, params: { models_<%= @subject_class.underscore %>: valid_attributes }
205
+ end
206
+ end
207
+
208
+ context 'with invalid params' do
209
+ let(:errors) { { <%= @fields.first.first %>: ['cannot be blank'] } }
210
+ let(:failure_result) { Dry::Monads::Failure(errors) }
211
+
212
+ before do
213
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('create')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!).and_return(true)
214
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('create')].reject { |c| c.empty? }.join("::") %>).to receive(:new).and_return(use_case_instance)
215
+ allow(use_case_instance).to receive(:result).and_return(failure_result)
216
+ allow(controller).to receive(:build_form_errors).and_return(<%= @model_class %>.new)
217
+ end
218
+
219
+ it 'does not create and renders new with errors' do
220
+ post :create, params: { models_<%= @subject_class.underscore %>: invalid_attributes }
221
+
222
+ expect(response).to have_http_status(:unprocessable_entity)
223
+ expect(response).to render_template('<%= [@route_scope_path, @scope_path, 'new'].reject { |c| c.empty? }.join("/") %>')
224
+ expect(assigns(:<%= @variable_subject %>)).to be_a(<%= @model_class %>)
225
+ end
226
+
227
+ it 'calls build_form_errors with correct parameters' do
228
+ expect(controller).to receive(:build_form_errors)
229
+ .with(anything, an_instance_of(<%= @model_class %>), errors)
230
+
231
+ post :create, params: { models_<%= @subject_class.underscore %>: invalid_attributes }
232
+ end
233
+ end
234
+ end
235
+
236
+ describe 'PATCH #update' do
237
+ let(:<%= @variable_subject %>) { create(:<%= @variable_subject %><%= @resource_owner_id.present? ? ", #{@resource_owner_id}: owner.id" : '' %>) }
238
+ let(:use_case_instance) { instance_double(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('update')].reject { |c| c.empty? }.join("::") %>) }
239
+ let(:updated_attributes) { { <%= @fields.first.first %>: 'Updated Value' } }
240
+
241
+ before do
242
+ allow(<%= @model_class %>).to receive(:find_by)
243
+ .with(<%= @resource_owner_id.present? ? "id: #{@variable_subject}.id, #{@resource_owner_id}: owner.id" : "id: #{@variable_subject}.id" %>)
244
+ .and_return(<%= @variable_subject %>)
245
+ end
246
+
247
+ context 'with valid params' do
248
+ let(:updated_<%= @variable_subject %>) { <%= @variable_subject %>.tap { |obj| obj.<%= @fields.first.first %> = 'Updated Value' } }
249
+ let(:success_result) { Dry::Monads::Success(updated_<%= @variable_subject %>) }
250
+
251
+ before do
252
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('update')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!).and_return(true)
253
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('update')].reject { |c| c.empty? }.join("::") %>).to receive(:new).and_return(use_case_instance)
254
+ allow(use_case_instance).to receive(:result).and_return(success_result)
255
+ end
256
+
257
+ it 'updates and redirects with success message' do
258
+ patch :update, params: { id: <%= @variable_subject %>.id, models_<%= @subject_class.underscore %>: updated_attributes }
259
+
260
+ expect(response).to redirect_to(<%= [@route_scope_path, @scope_path.singularize].reject { |c| c.empty? }.join("_") %>_url(id: <%= @variable_subject %>.id))
261
+ expect(flash[:success]).to eq('<%= @subject_class %> was successfully updated.')
262
+ end
263
+
264
+ it 'calls use case with correct contract' do
265
+ expect(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('update')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!)
266
+ .with(hash_including(<%= @resource_owner_id.present? ? "#{@resource_owner_id}: owner.id, " : '' %>id: <%= @variable_subject %>.id, **updated_attributes.stringify_keys))
267
+
268
+ patch :update, params: { id: <%= @variable_subject %>.id, models_<%= @subject_class.underscore %>: updated_attributes }
269
+ end
270
+ end
271
+
272
+ context 'with invalid params' do
273
+ let(:errors) { { <%= @fields.first.first %>: ['cannot be blank'] } }
274
+ let(:failure_result) { Dry::Monads::Failure(errors) }
275
+
276
+ before do
277
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('update')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!).and_return(true)
278
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('update')].reject { |c| c.empty? }.join("::") %>).to receive(:new).and_return(use_case_instance)
279
+ allow(use_case_instance).to receive(:result).and_return(failure_result)
280
+ allow(<%= @model_class %>).to receive(:find).with(<%= @variable_subject %>.id).and_return(<%= @variable_subject %>)
281
+ allow(controller).to receive(:build_form_errors).and_return(<%= @variable_subject %>)
282
+ end
283
+
284
+ it 'does not update and renders edit with errors' do
285
+ patch :update, params: { id: <%= @variable_subject %>.id, models_<%= @subject_class.underscore %>: invalid_attributes }
286
+
287
+ expect(response).to have_http_status(:unprocessable_entity)
288
+ expect(response).to render_template('<%= [@route_scope_path, @scope_path, 'edit'].reject { |c| c.empty? }.join("/") %>')
289
+ expect(assigns(:<%= @variable_subject %>)).to eq(<%= @variable_subject %>)
290
+ end
291
+ end
292
+ end
293
+
294
+ describe 'DELETE #destroy' do
295
+ let(:<%= @variable_subject %>) { create(:<%= @variable_subject %><%= @resource_owner_id.present? ? ", #{@resource_owner_id}: owner.id" : '' %>) }
296
+ let(:use_case_instance) { instance_double(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('destroy')].reject { |c| c.empty? }.join("::") %>) }
297
+
298
+ context 'when deletion succeeds' do
299
+ let(:success_result) { Dry::Monads::Success(true) }
300
+
301
+ before do
302
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('destroy')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!).and_return(true)
303
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('destroy')].reject { |c| c.empty? }.join("::") %>).to receive(:new).and_return(use_case_instance)
304
+ allow(use_case_instance).to receive(:result).and_return(success_result)
305
+ end
306
+
307
+ it 'destroys and redirects with success notice' do
308
+ delete :destroy, params: { id: <%= @variable_subject %>.id }
309
+
310
+ expect(response).to redirect_to(<%= [@route_scope_path, @scope_path].reject { |c| c.empty? }.join("_") %>_url)
311
+ expect(flash[:notice]).to eq('<%= @subject_class %> was successfully destroyed.')
312
+ end
313
+
314
+ it 'calls use case with correct contract' do
315
+ expect(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('destroy')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!)
316
+ .with(hash_including(<%= @resource_owner_id.present? ? "#{@resource_owner_id}: owner.id, " : '' %>id: <%= @variable_subject %>.id))
317
+
318
+ delete :destroy, params: { id: <%= @variable_subject %>.id }
319
+ end
320
+ end
321
+
322
+ context 'when deletion fails' do
323
+ let(:failure_result) { Dry::Monads::Failure('Cannot delete <%= @variable_subject %>') }
324
+
325
+ before do
326
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('destroy')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!).and_return(true)
327
+ allow(Core::UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('destroy')].reject { |c| c.empty? }.join("::") %>).to receive(:new).and_return(use_case_instance)
328
+ allow(use_case_instance).to receive(:result).and_return(failure_result)
329
+ end
330
+
331
+ it 'redirects with error message' do
332
+ delete :destroy, params: { id: <%= @variable_subject %>.id }
333
+
334
+ expect(response).to redirect_to(<%= [@route_scope_path, @scope_path].reject { |c| c.empty? }.join("_") %>_url)
335
+ expect(flash[:error]).to eq('<%= @subject_class %> could not be destroyed.')
336
+ end
337
+ end
338
+ end
339
+
340
+ describe 'private methods' do
341
+ describe '#build_contract' do
342
+ it 'merges <%= @resource_owner_id.present? ? "#{@resource_owner_id} " : '' %>with params' do
343
+ params = { <%= @fields.first.first %>: 'Test' }
344
+ result = controller.send(:build_contract, params)
345
+
346
+ expect(result).to include(<%= @resource_owner_id.present? ? "#{@resource_owner_id}: owner.id, " : '' %><%= @fields.first.first %>: 'Test')
347
+ end
348
+ end
349
+
350
+ describe '#set_<%= @variable_subject %>' do
351
+ context 'when <%= @variable_subject %> exists' do
352
+ let(:<%= @variable_subject %>) { create(:<%= @variable_subject %><%= @resource_owner_id.present? ? ", #{@resource_owner_id}: owner.id" : '' %>) }
353
+
354
+ before do
355
+ allow(<%= @model_class %>).to receive(:find_by)
356
+ .with(<%= @resource_owner_id.present? ? "id: #{@variable_subject}.id, #{@resource_owner_id}: owner.id" : "id: #{@variable_subject}.id" %>)
357
+ .and_return(<%= @variable_subject %>)
358
+ end
359
+
360
+ it 'sets @<%= @variable_subject %>' do
361
+ controller.params[:id] = <%= @variable_subject %>.id
362
+ controller.send(:set_<%= @variable_subject %>)
363
+
364
+ expect(assigns(:<%= @variable_subject %>)).to eq(<%= @variable_subject %>)
365
+ expect(response).not_to be_redirect
366
+ end
367
+ end
368
+
369
+ context 'when <%= @variable_subject %> does not exist' do
370
+ before do
371
+ allow(<%= @model_class %>).to receive(:find_by)
372
+ .with(<%= @resource_owner_id.present? ? "id: 'non-existent', #{@resource_owner_id}: owner.id" : "id: 'non-existent'" %>)
373
+ .and_return(nil)
374
+ end
375
+
376
+ it 'redirects with error' do
377
+ controller.params[:id] = 'non-existent'
378
+ controller.send(:set_<%= @variable_subject %>)
379
+
380
+ expect(response).to redirect_to(<%= [@route_scope_path, @scope_path].reject { |c| c.empty? }.join("_") %>_url)
381
+ expect(flash[:error]).to eq('<%= @subject_class %> not found.')
382
+ end
383
+ end
384
+ end
385
+
386
+ describe '#<%= @variable_subject %>_params' do
387
+ it 'permits correct attributes' do
388
+ params = ActionController::Parameters.new(
389
+ models_<%= @subject_class.underscore %>: valid_attributes
390
+ )
391
+ allow(controller).to receive(:params).and_return(params)
392
+
393
+ permitted = controller.send(:<%= @variable_subject %>_params)
394
+ expect(permitted).to be_permitted
395
+ end
396
+ end
397
+ end
398
+ end
@@ -1,5 +1,5 @@
1
1
  <div>
2
- <div>
2
+ <div class="pt-5">
3
3
  <%%= render "components/alert" %>
4
4
  </div>
5
5
  <div class="flow-root class-card-container">
@@ -9,7 +9,7 @@
9
9
  <p class="mt-2 text-sm text-gray-700">A list of all the <%= @subject_class %>.</p>
10
10
  </div>
11
11
  <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
12
- <%% if Rails.application.routes.url_helpers.respond_to?(:new_<%= [@route_scope_path, @scope_path.singularize].reject { |c| c.empty? }.join("_") %>_path) %>
12
+ <%% if respond_to?(:new_<%= [@route_scope_path, @scope_path.singularize].reject { |c| c.empty? }.join("_") %>_path) %>
13
13
  <%%= link_to new_<%= [@route_scope_path, @scope_path.singularize].reject { |c| c.empty? }.join("_") %>_path do %>
14
14
  <button type="button" class="block class-button">Add <%= @subject_class %></button>
15
15
  <%% end %>
@@ -22,14 +22,14 @@
22
22
  <thead>
23
23
  <tr>
24
24
  <%% <%= @controllers.list_fields.map { |tc| [tc.titleize.to_s, '']} + [["Action", "text-center"]]%>.each do |title, attr_class| %>
25
- <th scope="col" class="class-tr attr-class"><%%= title %></th>
25
+ <th scope="col" class="class-tr <%%= attr_class %>"><%%= title %></th>
26
26
  <%% end %>
27
27
  </th>
28
28
  </tr>
29
29
  </thead>
30
- <tbody class="divide-y bg-white">
30
+ <tbody class="min-w-full divide-y divide-gray-100">
31
31
  <%% @<%= @variable_subject.pluralize %>.response.each do |<%= @variable_subject %>| %>
32
- <tr>
32
+ <tr class="even:bg-gray-50">
33
33
  <%@controllers.list_fields.each do |field| -%>
34
34
  <td class="class-td">
35
35
  <div class="flex items-center">
@@ -1,4 +1,7 @@
1
1
  <div>
2
+ <div class="pt-5">
3
+ <%%= render "components/alert" %>
4
+ </div>
2
5
  <div class="flow-root class-card-container">
3
6
  <div class="flex items-center p-2 mb-10">
4
7
  <div class="flex-auto">
@@ -0,0 +1,42 @@
1
+ ---
2
+ model: TestModel
3
+ resource_name: test_models
4
+ actor: admin
5
+ resource_owner_id: user_id
6
+ entity:
7
+ skipped_fields:
8
+ - created_at
9
+ - updated_at
10
+ custom_fields: []
11
+ domains:
12
+ action_list:
13
+ use_case:
14
+ contract:
15
+ - name
16
+ - email
17
+ action_fetch_by_id:
18
+ use_case:
19
+ contract:
20
+ - id
21
+ - name
22
+ - email
23
+ action_create:
24
+ use_case:
25
+ contract:
26
+ - name
27
+ - email
28
+ action_update:
29
+ use_case:
30
+ contract:
31
+ - name
32
+ - email
33
+ action_destroy:
34
+ use_case:
35
+ contract:
36
+ - id
37
+ controllers:
38
+ form_fields:
39
+ - name: name
40
+ type: string
41
+ - name: email
42
+ type: string
data/lib/sun-sword.rb CHANGED
@@ -6,4 +6,5 @@ require 'sun_sword/configuration'
6
6
  module SunSword
7
7
  extend SunSword::Configuration
8
8
  define_setting :scope_owner_column, ''
9
+ define_setting :scope_owner, ''
9
10
  end