sun-sword 0.0.11 → 0.0.13

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 +193 -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 +374 -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 < 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 = <%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %><%= [@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 '<%= [@route_scope_path.singularize].reject { |c| c.empty? }.join("_") + "/" %><%= [@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 = <%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %><%= [@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 = <%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %><%= [@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 <%= [@route_scope_path.singularize].reject { |c| c.empty? }.join("_") + "_" %><%= [@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 '<%= [@route_scope_path.singularize].reject { |c| c.empty? }.join("_") + "/" %><%= [@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 = <%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %><%= [@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 <%= [@route_scope_path.singularize].reject { |c| c.empty? }.join("_") + "_" %><%= [@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 '<%= [@route_scope_path.singularize].reject { |c| c.empty? }.join("_") + "/" %><%= [@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 = <%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %><%= [@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 <%= [@route_scope_path.singularize].reject { |c| c.empty? }.join("_") + "_" %><%= [@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 <%= [@route_scope_path.singularize].reject { |c| c.empty? }.join("_") + "_" %><%= [@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 <%= [@route_scope_path.singularize].reject { |c| c.empty? }.join("_") + "_" %><%= [@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,374 @@
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, <%= @resource_owner_id%>: owner.id) }
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 %>, 3) }
47
+
48
+ before do
49
+ stub_use_case(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('list')].reject { |c| c.empty? }.join("::") %>, <%= @variable_subject.pluralize %>, response: :success)
50
+ end
51
+
52
+ it 'returns success response and assigns @<%= @variable_subject.pluralize %>' do
53
+ get :index
54
+
55
+ expect(response).to have_http_status(:success)
56
+ expect(assigns(:<%= @variable_subject.pluralize %>)).to eq(<%= @variable_subject.pluralize %>)
57
+ expect(response).to render_template('<%= [@route_scope_path, @scope_path, 'index'].reject { |c| c.empty? }.join("/") %>')
58
+ end
59
+
60
+ it 'calls use case with correct contract' do
61
+ expect(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>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))" : '' %>
62
+
63
+ get :index
64
+ end
65
+ end
66
+
67
+ context 'when use case fails' do
68
+ let(:use_case_instance) { instance_double(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('list')].reject { |c| c.empty? }.join("::") %>) }
69
+ let(:failure_result) { 'Error loading <%= @variable_subject.pluralize %>' }
70
+
71
+ before do
72
+ stub_use_case(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('list')].reject { |c| c.empty? }.join("::") %>, failure_result, response: :failure)
73
+ end
74
+
75
+ it 'redirects to root path with error' do
76
+ get :index
77
+
78
+ expect(response).to redirect_to(root_path)
79
+ expect(flash[:success]).to eq('Error loading <%= @variable_subject.pluralize %>')
80
+ end
81
+ end
82
+ end
83
+
84
+ describe 'GET #show' do
85
+ let(:<%= @variable_subject %>) { create(:<%= @variable_subject %><%= @resource_owner_id.present? ? ", #{@resource_owner_id}: owner.id" : '' %>) }
86
+
87
+ context 'when use case succeeds' do
88
+ let(:use_case_instance) { instance_double(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>) }
89
+ let(:success_result) { <%= @variable_subject %> }
90
+
91
+ before do
92
+ stub_use_case(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>, success_result, response: :success)
93
+ end
94
+
95
+ it 'returns success response and assigns @<%= @variable_subject %>' do
96
+ get :show, params: { id: <%= @variable_subject %>.id }
97
+
98
+ expect(response).to have_http_status(:success)
99
+ expect(assigns(:<%= @variable_subject %>)).to eq(<%= @variable_subject %>)
100
+ end
101
+
102
+ it 'calls use case with correct contract' do
103
+ expect(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!)
104
+ .with(hash_including(<%= @resource_owner_id.present? ? "#{@resource_owner_id}: owner.id, " : '' %>id: <%= @variable_subject %>.id))
105
+
106
+ get :show, params: { id: <%= @variable_subject %>.id }
107
+ end
108
+ end
109
+
110
+ context 'when use case fails' do
111
+ let(:use_case_instance) { instance_double(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>) }
112
+ let(:failure_result) { '<%= @subject_class %> not found' }
113
+
114
+ before do
115
+ stub_use_case(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('fetch', '_by_id')].reject { |c| c.empty? }.join("::") %>, failure_result, response: :failure)
116
+ end
117
+
118
+ it 'redirects to root path with error' do
119
+ get :show, params: { id: 'non-existent' }
120
+
121
+ expect(response).to redirect_to(root_path)
122
+ expect(flash[:success]).to eq('<%= @subject_class %> not found')
123
+ end
124
+ end
125
+ end
126
+
127
+ describe 'GET #new' do
128
+ it 'returns success response and assigns new <%= @variable_subject %>' do
129
+ get :new
130
+
131
+ expect(response).to have_http_status(:success)
132
+ expect(assigns(:<%= @variable_subject %>)).to be_a_new(<%= @model_class %>)
133
+ end
134
+ end
135
+
136
+ describe 'GET #edit' do
137
+ let(:<%= @variable_subject %>) { create(:<%= @variable_subject %><%= @resource_owner_id.present? ? ", #{@resource_owner_id}: owner.id" : '' %>) }
138
+
139
+ context 'when <%= @variable_subject %> exists<%= @resource_owner_id.present? ? " and belongs to owner" : '' %>' do
140
+ before do
141
+ allow(<%= @model_class %>).to receive(:find_by)
142
+ .with(<%= @resource_owner_id.present? ? "id: #{@variable_subject}.id, #{@resource_owner_id}: owner.id" : "id: #{@variable_subject}.id" %>)
143
+ .and_return(<%= @variable_subject %>)
144
+ end
145
+
146
+ it 'returns success response and assigns <%= @variable_subject %>' do
147
+ get :edit, params: { id: <%= @variable_subject %>.id }
148
+
149
+ expect(response).to have_http_status(:success)
150
+ expect(assigns(:<%= @variable_subject %>)).to eq(<%= @variable_subject %>)
151
+ end
152
+ end
153
+
154
+ context 'when <%= @variable_subject %> does not exist<%= @resource_owner_id.present? ? " or does not belong to owner" : '' %>' do
155
+ before do
156
+ allow(<%= @model_class %>).to receive(:find_by)
157
+ .with(<%= @resource_owner_id.present? ? "id: 'non-existent', #{@resource_owner_id}: owner.id" : "id: 'non-existent'" %>)
158
+ .and_return(nil)
159
+ end
160
+
161
+ it 'redirects with error' do
162
+ get :edit, params: { id: 'non-existent' }
163
+
164
+ expect(response).to redirect_to(<%= [@route_scope_path, @scope_path].reject { |c| c.empty? }.join("_") %>_url)
165
+ expect(flash[:error]).to eq('<%= @subject_class %> not found.')
166
+ end
167
+ end
168
+ end
169
+
170
+ describe 'POST #create' do
171
+ let(:use_case_instance) { instance_double(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('create')].reject { |c| c.empty? }.join("::") %>) }
172
+
173
+ context 'with valid params' do
174
+ let(:success_result) { Hashie::Mash.new({ id: SecureRandom.uuid }) }
175
+
176
+ before do
177
+ stub_use_case(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('create')].reject { |c| c.empty? }.join("::") %>, success_result, response: :success)
178
+ end
179
+
180
+ it 'creates and redirects with success message' do
181
+ post :create, params: { models_<%= @subject_class.underscore %>: valid_attributes }
182
+
183
+ expect(response).to redirect_to(<%= [@route_scope_path, @scope_path.singularize].reject { |c| c.empty? }.join("_") %>_url(id: success_result.id))
184
+ expect(flash[:success]).to eq('<%= @subject_class %> was successfully created.')
185
+ end
186
+
187
+ it 'calls use case with correct contract' do
188
+ expect(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('create')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!)
189
+ .with(hash_including(<%= @resource_owner_id.present? ? "#{@resource_owner_id}: owner.id, " : '' %>**valid_attributes.stringify_keys))
190
+
191
+ post :create, params: { models_<%= @subject_class.underscore %>: valid_attributes }
192
+ end
193
+ end
194
+
195
+ context 'with invalid params' do
196
+ let(:errors) { { <%= @fields.first.first %>: ['cannot be blank'] } }
197
+ let(:failure_result) { errors}
198
+
199
+ before do
200
+ stub_use_case(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('create')].reject { |c| c.empty? }.join("::") %>, failure_result, response: :failure)
201
+ allow(controller).to receive(:build_form_errors).and_return(<%= @model_class %>.new)
202
+ end
203
+
204
+ it 'does not create and renders new with errors' do
205
+ post :create, params: { models_<%= @subject_class.underscore %>: invalid_attributes }
206
+
207
+ expect(response).to have_http_status(:unprocessable_entity)
208
+ expect(response).to render_template('<%= [@route_scope_path, @scope_path, 'new'].reject { |c| c.empty? }.join("/") %>')
209
+ expect(assigns(:<%= @variable_subject %>)).to be_a(<%= @model_class %>)
210
+ end
211
+
212
+ it 'calls build_form_errors with correct parameters' do
213
+ expect(controller).to receive(:build_form_errors)
214
+ .with(anything, an_instance_of(<%= @model_class %>), errors)
215
+
216
+ post :create, params: { models_<%= @subject_class.underscore %>: invalid_attributes }
217
+ end
218
+ end
219
+ end
220
+
221
+ describe 'PATCH #update' do
222
+ let(:<%= @variable_subject %>) { create(:<%= @variable_subject %><%= @resource_owner_id.present? ? ", #{@resource_owner_id}: owner.id" : '' %>) }
223
+ let(:updated_attributes) { { <%= @fields.first.first %>: 'Updated Value' } }
224
+
225
+ before do
226
+ allow(<%= @model_class %>).to receive(:find_by)
227
+ .with(<%= @resource_owner_id.present? ? "id: #{@variable_subject}.id, #{@resource_owner_id}: owner.id" : "id: #{@variable_subject}.id" %>)
228
+ .and_return(<%= @variable_subject %>)
229
+ end
230
+
231
+ context 'with valid params' do
232
+ let(:updated_<%= @variable_subject %>) { <%= @variable_subject %>.tap { |obj| obj.<%= @fields.first.first %> = 'Updated Value' } }
233
+
234
+ before do
235
+ stub_use_case(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('update')].reject { |c| c.empty? }.join("::") %>, updated_campaign, response: :success)
236
+ end
237
+
238
+ it 'updates and redirects with success message' do
239
+ patch :update, params: { id: <%= @variable_subject %>.id, models_<%= @subject_class.underscore %>: updated_attributes }
240
+
241
+ expect(response).to redirect_to(<%= [@route_scope_path, @scope_path.singularize].reject { |c| c.empty? }.join("_") %>_url(id: <%= @variable_subject %>.id))
242
+ expect(flash[:success]).to eq('<%= @subject_class %> was successfully updated.')
243
+ end
244
+
245
+ it 'calls use case with correct contract' do
246
+ expect(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('update')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!)
247
+ .with(hash_including(<%= @resource_owner_id.present? ? "#{@resource_owner_id}: owner.id, " : '' %>id: <%= @variable_subject %>.id, **updated_attributes.stringify_keys))
248
+
249
+ patch :update, params: { id: <%= @variable_subject %>.id, models_<%= @subject_class.underscore %>: updated_attributes }
250
+ end
251
+ end
252
+
253
+ context 'with invalid params' do
254
+ let(:errors) { { <%= @fields.first.first %>: ['cannot be blank'] } }
255
+ let(:failure_result) { errors }
256
+
257
+ before do
258
+ stub_use_case(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('update')].reject { |c| c.empty? }.join("::") %>, failure_result, response: :failure)
259
+ allow(<%= @model_class %>).to receive(:find).with(<%= @variable_subject %>.id).and_return(<%= @variable_subject %>)
260
+ allow(controller).to receive(:build_form_errors).and_return(<%= @variable_subject %>)
261
+ end
262
+
263
+ it 'does not update and renders edit with errors' do
264
+ patch :update, params: { id: <%= @variable_subject %>.id, models_<%= @subject_class.underscore %>: invalid_attributes }
265
+
266
+ expect(response).to have_http_status(:unprocessable_entity)
267
+ expect(response).to render_template('<%= [@route_scope_path, @scope_path, 'edit'].reject { |c| c.empty? }.join("/") %>')
268
+ expect(assigns(:<%= @variable_subject %>)).to eq(<%= @variable_subject %>)
269
+ end
270
+ end
271
+ end
272
+
273
+ describe 'DELETE #destroy' do
274
+ let(:<%= @variable_subject %>) { create(:<%= @variable_subject %><%= @resource_owner_id.present? ? ", #{@resource_owner_id}: owner.id" : '' %>) }
275
+
276
+ context 'when deletion succeeds' do
277
+ let(:success_result) { true }
278
+
279
+ before do
280
+ stub_use_case(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('destroy')].reject { |c| c.empty? }.join("::") %>, success_result, response: :success)
281
+ end
282
+
283
+ it 'destroys and redirects with success notice' do
284
+ delete :destroy, params: { id: <%= @variable_subject %>.id }
285
+
286
+ expect(response).to redirect_to(<%= [@route_scope_path, @scope_path].reject { |c| c.empty? }.join("_") %>_url)
287
+ expect(flash[:notice]).to eq('<%= @subject_class %> was successfully destroyed.')
288
+ end
289
+
290
+ it 'calls use case with correct contract' do
291
+ expect(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('destroy')].reject { |c| c.empty? }.join("::") %>).to receive(:contract!)
292
+ .with(hash_including(<%= @resource_owner_id.present? ? "#{@resource_owner_id}: owner.id, " : '' %>id: <%= @variable_subject %>.id))
293
+
294
+ delete :destroy, params: { id: <%= @variable_subject %>.id }
295
+ end
296
+ end
297
+
298
+ context 'when deletion fails' do
299
+ let(:failure_result) { 'Cannot delete <%= @variable_subject %>' }
300
+
301
+ before do
302
+ stub_use_case(<%= [@usecase_domain].reject { |c| c.empty? }.join("::").sub(/(.+)/, '\1::') %>UseCases::<%= [@route_scope_class, @scope_class, build_usecase_filename('destroy')].reject { |c| c.empty? }.join("::") %>, failure_result, response: :failure)
303
+ end
304
+
305
+ it 'redirects with error message' do
306
+ delete :destroy, params: { id: <%= @variable_subject %>.id }
307
+
308
+ expect(response).to redirect_to(<%= [@route_scope_path, @scope_path].reject { |c| c.empty? }.join("_") %>_url)
309
+ expect(flash[:error]).to eq('<%= @subject_class %> could not be destroyed.')
310
+ end
311
+ end
312
+ end
313
+
314
+ describe 'private methods' do
315
+ describe '#build_contract' do
316
+ it 'merges <%= @resource_owner_id.present? ? "#{@resource_owner_id} " : '' %>with params' do
317
+ params = { <%= @fields.first.first %>: 'Test' }
318
+ result = controller.send(:build_contract, params)
319
+
320
+ expect(result).to include(<%= @resource_owner_id.present? ? "#{@resource_owner_id}: owner.id, " : '' %><%= @fields.first.first %>: 'Test')
321
+ end
322
+ end
323
+
324
+ describe '#set_<%= @variable_subject %>' do
325
+ context 'when <%= @variable_subject %> exists' do
326
+ let(:<%= @variable_subject %>) { create(:<%= @variable_subject %><%= @resource_owner_id.present? ? ", #{@resource_owner_id}: owner.id" : '' %>) }
327
+
328
+ before do
329
+ allow(<%= @model_class %>).to receive(:find_by)
330
+ .with(<%= @resource_owner_id.present? ? "id: #{@variable_subject}.id, #{@resource_owner_id}: owner.id" : "id: #{@variable_subject}.id" %>)
331
+ .and_return(<%= @variable_subject %>)
332
+ end
333
+
334
+ it 'sets @<%= @variable_subject %>' do
335
+ controller.params[:id] = <%= @variable_subject %>.id
336
+ controller.send(:set_<%= @variable_subject %>)
337
+
338
+ expect(assigns(:<%= @variable_subject %>)).to eq(<%= @variable_subject %>)
339
+ expect(response).not_to be_redirect
340
+ end
341
+ end
342
+
343
+ context 'when <%= @variable_subject %> does not exist' do
344
+ before do
345
+ allow(<%= @model_class %>).to receive(:find_by)
346
+ .with(<%= @resource_owner_id.present? ? "id: 'non-existent', #{@resource_owner_id}: owner.id" : "id: 'non-existent'" %>)
347
+ .and_return(nil)
348
+ end
349
+
350
+ it 'redirects with error' do
351
+ controller.params[:id] = 'non-existent'
352
+ allow(controller).to receive(:redirect_to)
353
+
354
+ controller.send(:set_<%= @variable_subject %>)
355
+
356
+ expect(controller.instance_variable_get(:@<%= @variable_subject %>)).to be_nil
357
+ expect(controller).to have_received(:redirect_to).with(<%= [@route_scope_path, @scope_path].reject { |c| c.empty? }.join("_") %>_url, error: 'Campaign not found.')
358
+ end
359
+ end
360
+ end
361
+
362
+ describe '#<%= @variable_subject %>_params' do
363
+ it 'permits correct attributes' do
364
+ params = ActionController::Parameters.new(
365
+ models_<%= @subject_class.underscore %>: valid_attributes
366
+ )
367
+ allow(controller).to receive(:params).and_return(params)
368
+
369
+ permitted = controller.send(:<%= @variable_subject %>_params)
370
+ expect(permitted).to be_permitted
371
+ end
372
+ end
373
+ end
374
+ 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