steel_wheel 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +39 -0
  3. data/.qlty/.gitignore +7 -0
  4. data/.qlty/qlty.toml +82 -0
  5. data/.ruby-version +1 -1
  6. data/Gemfile +2 -1
  7. data/Gemfile.lock +148 -108
  8. data/README.md +627 -224
  9. data/lib/generators/steel_wheel/application_handler/templates/handler_template.rb +4 -0
  10. data/lib/generators/steel_wheel/form/USAGE +8 -0
  11. data/lib/generators/steel_wheel/form/form_generator.rb +16 -0
  12. data/lib/generators/steel_wheel/form/templates/form_template.rb +3 -0
  13. data/lib/generators/steel_wheel/handler/templates/handler_template.rb +5 -16
  14. data/lib/generators/steel_wheel/scaffold_controller/USAGE +14 -0
  15. data/lib/generators/steel_wheel/scaffold_controller/scaffold_controller_generator.rb +100 -0
  16. data/lib/generators/steel_wheel/scaffold_controller/templates/controller.rb +65 -0
  17. data/lib/generators/steel_wheel/scaffold_controller/templates/create.rb +23 -0
  18. data/lib/generators/steel_wheel/scaffold_controller/templates/form.tt +12 -0
  19. data/lib/generators/steel_wheel/scaffold_controller/templates/index.rb +17 -0
  20. data/lib/generators/steel_wheel/scaffold_controller/templates/model_form.rb +32 -0
  21. data/lib/generators/steel_wheel/scaffold_controller/templates/search_form.rb +31 -0
  22. data/lib/generators/steel_wheel/scaffold_controller/templates/update.rb +31 -0
  23. data/lib/steel_wheel/callbacks.rb +34 -0
  24. data/lib/steel_wheel/components.rb +54 -0
  25. data/lib/steel_wheel/filters.rb +36 -0
  26. data/lib/steel_wheel/handler.rb +67 -68
  27. data/lib/steel_wheel/params.rb +5 -1
  28. data/lib/steel_wheel/preconditions.rb +36 -0
  29. data/lib/steel_wheel/query/dependency_validator.rb +14 -0
  30. data/lib/steel_wheel/query/exists_validator.rb +15 -0
  31. data/lib/steel_wheel/query/verify_validator.rb +30 -0
  32. data/lib/steel_wheel/railtie.rb +50 -0
  33. data/lib/steel_wheel/shortcuts.rb +28 -0
  34. data/lib/steel_wheel/version.rb +1 -1
  35. data/lib/steel_wheel.rb +35 -6
  36. data/steel_wheel.gemspec +4 -4
  37. metadata +38 -30
  38. data/.github/workflows/ruby.yml +0 -43
  39. data/lib/generators/steel_wheel/command/USAGE +0 -8
  40. data/lib/generators/steel_wheel/command/command_generator.rb +0 -16
  41. data/lib/generators/steel_wheel/command/templates/command_template.rb +0 -5
  42. data/lib/generators/steel_wheel/params/USAGE +0 -8
  43. data/lib/generators/steel_wheel/params/params_generator.rb +0 -16
  44. data/lib/generators/steel_wheel/params/templates/params_template.rb +0 -5
  45. data/lib/generators/steel_wheel/query/USAGE +0 -8
  46. data/lib/generators/steel_wheel/query/query_generator.rb +0 -16
  47. data/lib/generators/steel_wheel/query/templates/query_template.rb +0 -5
  48. data/lib/steel_wheel/command.rb +0 -24
  49. data/lib/steel_wheel/query.rb +0 -20
  50. data/lib/steel_wheel/response.rb +0 -34
@@ -1,4 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ApplicationHandler < SteelWheel::Handler
4
+ url_params do
5
+ string :controller
6
+ string :action
7
+ end
4
8
  end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates operations files
3
+
4
+ Example:
5
+ rails generate steel_wheel:form Thing
6
+
7
+ This will create:
8
+ app/handlers/thing_form.rb
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SteelWheel
4
+ class FormGenerator < Rails::Generators::NamedBase
5
+ source_root File.expand_path('templates', __dir__)
6
+
7
+ def copy_files
8
+ if behavior == :revoke
9
+ template 'form_template.rb', "app/handlers/#{file_path}_form.rb"
10
+ elsif behavior == :invoke
11
+ empty_directory Pathname.new('app/handlers').join(*class_path)
12
+ template 'form_template.rb', "app/handlers/#{file_path}_form.rb"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ class <%= class_name %>Form < ActionForm::Rails::Base
2
+
3
+ end
@@ -1,21 +1,10 @@
1
1
  class <%= class_name %>Handler < ApplicationHandler
2
- define do
3
- params do
4
-
5
- end
6
-
7
- query do
8
-
9
- end
10
-
11
- command do
12
- def call(*)
13
- # NOOP
14
- end
15
- end
2
+ def call
3
+ # NOOP
16
4
  end
17
5
 
18
- def on_success(flow)
19
- flow.call(flow)
6
+ def on_validation_success
7
+ # call if current_action.create?
8
+ # update if current_action.update?
20
9
  end
21
10
  end
@@ -0,0 +1,14 @@
1
+ Description:
2
+ Generates controller and handler files for a resource
3
+
4
+ Example:
5
+ rails generate steel_wheel:scaffold_controller Thing name:string age:integer active:boolean
6
+
7
+ This will create:
8
+ app/controllers/things_controller.rb
9
+ app/handlers/things/index_handler.rb
10
+ app/handlers/things/create_handler.rb
11
+ app/handlers/things/update_handler.rb
12
+ app/handlers/things/model_form.rb
13
+
14
+ The generated handlers will include appropriate params definitions based on the provided attributes.
@@ -0,0 +1,100 @@
1
+ module SteelWheel
2
+ class ScaffoldControllerGenerator < Rails::Generators::NamedBase
3
+ include Rails::Generators::ResourceHelpers
4
+
5
+ source_root File.expand_path('templates', __dir__)
6
+
7
+ check_class_collision suffix: "Controller"
8
+
9
+ class_option :helper, type: :boolean
10
+ class_option :orm, banner: "NAME", type: :string, required: true,
11
+ desc: "ORM to generate the controller for"
12
+ class_option :api, type: :boolean,
13
+ desc: "Generate API controller"
14
+
15
+ class_option :skip_routes, type: :boolean, desc: "Don't add routes to config/routes.rb."
16
+
17
+ argument :attributes, type: :array, default: [], banner: "field:type field:type"
18
+
19
+ def create_controller_files
20
+ template_file = options.api? ? "api_controller.rb" : "controller.rb"
21
+ template template_file, File.join("app/controllers", controller_class_path, "#{controller_file_name}_controller.rb")
22
+ end
23
+
24
+ def copy_template
25
+ if behavior == :revoke
26
+ template 'form.tt', "lib/templates/erb/scaffold/_form.html.erb.tt"
27
+ elsif behavior == :invoke
28
+ create_file "lib/templates/erb/scaffold/_form.html.erb.tt", File.read(File.join(__dir__, 'templates', 'form.tt'))
29
+ end
30
+ end
31
+
32
+ def copy_files
33
+ if behavior == :revoke
34
+ template 'index.rb', "app/handlers/#{controller_file_path}/index_handler.rb"
35
+ template 'create.rb', "app/handlers/#{controller_file_path}/create_handler.rb"
36
+ template 'update.rb', "app/handlers/#{controller_file_path}/update_handler.rb"
37
+ template 'model_form.rb', "app/handlers/#{controller_file_path}/model_form.rb"
38
+ template 'search_form.rb', "app/handlers/#{controller_file_path}/search_form.rb"
39
+ elsif behavior == :invoke
40
+ empty_directory Pathname.new('app/handlers').join(controller_file_path)
41
+ template 'index.rb', "app/handlers/#{controller_file_path}/index_handler.rb"
42
+ template 'create.rb', "app/handlers/#{controller_file_path}/create_handler.rb"
43
+ template 'update.rb', "app/handlers/#{controller_file_path}/update_handler.rb"
44
+ template 'model_form.rb', "app/handlers/#{controller_file_path}/model_form.rb"
45
+ template 'search_form.rb', "app/handlers/#{controller_file_path}/search_form.rb"
46
+ end
47
+ end
48
+
49
+ hook_for :template_engine, as: :scaffold do |template_engine|
50
+ invoke template_engine unless options.api?
51
+ end
52
+
53
+ hook_for :resource_route, required: true do |route|
54
+ invoke route unless options.skip_routes?
55
+ end
56
+
57
+ hook_for :test_framework, as: :scaffold
58
+
59
+ # Invoke the helper using the controller name (pluralized)
60
+ hook_for :helper, as: :scaffold do |invoked|
61
+ invoke invoked, [ controller_name ]
62
+ end
63
+
64
+ private
65
+
66
+ def convert_to_easy_params_type(attr)
67
+ case attr.type
68
+ when :attachment, :attachments then ':array, of: :string'
69
+ when :belongs_to, :references then :integer # Assuming these are foreign keys
70
+ when :boolean then :bool
71
+ when :date then :date
72
+ when :datetime, :timestamp then :datetime
73
+ when :decimal then :decimal
74
+ when :digest then :string # Assuming this is for password digests
75
+ when :float then :float
76
+ when :integer then :integer
77
+ when :rich_text then :string # Or consider a custom RichText type
78
+ when :string, :text then :string
79
+ when :time then :time
80
+ when :token then :string # Or consider a custom Token type
81
+ end
82
+ end
83
+
84
+ def permitted_params
85
+ attachments, others = attributes_names.partition { |name| attachments?(name) }
86
+ params = others.map { |name| ":#{name}" }
87
+ params += attachments.map { |name| "#{name}: []" }
88
+ params.join(", ")
89
+ end
90
+
91
+ def simple_params
92
+ attributes.reject { |attr| attachments?(attr.name) }
93
+ end
94
+
95
+ def attachments?(name)
96
+ attribute = attributes.find { |attr| attr.name == name }
97
+ attribute&.attachments?
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,65 @@
1
+ class <%= controller_class_name %>Controller < ApplicationController
2
+ before_action :authenticate_account!
3
+
4
+ # GET <%= route_url %>
5
+ action :index do |handler|
6
+ @<%= controller_file_name %> = handler.<%= controller_file_name %>
7
+ @form = handler.form
8
+ end
9
+
10
+ # GET <%= route_url %>/1
11
+ action :show, handler: :update do |handler|
12
+ @<%= file_name %> = handler.<%= file_name %>
13
+ end
14
+
15
+ # GET <%= route_url %>/new
16
+ action :new, handler: :create do |handler|
17
+ @<%= file_name %> = handler.<%= file_name %>
18
+ @form = handler.form
19
+ end
20
+
21
+ # GET <%= route_url %>/1/edit
22
+ action :edit, handler: :update do |handler|
23
+ @<%= file_name %> = handler.<%= file_name %>
24
+ @form = handler.form
25
+ end
26
+
27
+ # POST <%= route_url %>
28
+ action :create do |handler|
29
+ handler.success do
30
+ <%- if controller_class_path.empty? -%>
31
+ redirect_to handler.<%= file_name %>, notice: '<%= human_name %> was successfully created.'
32
+ <%- else -%>
33
+ redirect_to [<%= controller_class_path.map{|path| ":#{path}"}.join(', ') %>, handler.<%= file_name %>], notice: '<%= human_name %> was successfully created.'
34
+ <%- end -%>
35
+ end
36
+
37
+ handler.failure do
38
+ @<%= file_name %> = handler.<%= file_name %>
39
+ @form = handler.form
40
+ render :new
41
+ end
42
+ end
43
+
44
+ # PATCH/PUT <%= route_url %>/1
45
+ action :update do |handler|
46
+ handler.success do
47
+ <%- if controller_class_path.empty? -%>
48
+ redirect_to handler.<%= file_name %>, notice: '<%= human_name %> was successfully updated.'
49
+ <%- else -%>
50
+ redirect_to [<%= controller_class_path.map{|path| ":#{path}"}.join(', ') %>, handler.<%= file_name %>], notice: '<%= human_name %> was successfully updated.'
51
+ <%- end -%>
52
+ end
53
+
54
+ handler.failure do
55
+ @form = handler.form
56
+ @<%= file_name %> = handler.<%= file_name %>
57
+ render :edit
58
+ end
59
+ end
60
+
61
+ # DELETE <%= route_url %>/1
62
+ action :destroy, handler: :update do |handler|
63
+ redirect_to <%= index_helper %>_url, notice: '<%= human_name %> was successfully destroyed.'
64
+ end
65
+ end
@@ -0,0 +1,23 @@
1
+ class <%= controller_class_name %>::CreateHandler < ApplicationHandler
2
+ form <%= controller_class_name %>::ModelForm
3
+
4
+ verify memoize def <%= file_name %>
5
+ <%= singular_name.camelize %>.new(form_params.to_h)
6
+ end
7
+
8
+ def form_attributes
9
+ <%- if controller_class_path.empty? -%>
10
+ { model: <%= file_name %> }
11
+ <%- else -%>
12
+ { model: [<%= controller_class_path.map{|path| ":#{path}"}.join(', ') %>, <%= file_name %>] }
13
+ <%- end -%>
14
+ end
15
+
16
+ def on_validation_success
17
+ call if current_action.create?
18
+ end
19
+
20
+ def call
21
+ <%= file_name %>.save
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ <%% if @errors.any? %>
2
+ <div style="color: red">
3
+ <h2><%%= pluralize(@errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
4
+
5
+ <ul>
6
+ <%% @errors.each do |error| %>
7
+ <li><%%= error.full_message %></li>
8
+ <%% end %>
9
+ </ul>
10
+ </div>
11
+ <%% end %>
12
+ <%%= render @form %>
@@ -0,0 +1,17 @@
1
+ class <%= controller_class_name %>::IndexHandler < ApplicationHandler
2
+ def form_attributes
3
+ { method: :get, action: helpers.<%= index_helper %>_path, params: form_params }
4
+ end
5
+
6
+ form <%= controller_class_name %>::SearchForm
7
+ <%- simple_params.each do |attribute| -%>
8
+
9
+ filter :<%= attribute.column_name %> do |scope, value|
10
+ scope.where(<%= attribute.column_name %>: value)
11
+ end
12
+ <%- end -%>
13
+
14
+ filterable def <%= controller_file_name %>
15
+ <%= singular_name.camelize %>.all
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ module <%= controller_class_name %>
2
+ class ModelForm < ActionForm::Rails::Base
3
+ resource_model <%= singular_name.camelize %>
4
+ <%- simple_params.each do |attribute| -%>
5
+
6
+ element :<%= attribute.column_name %> do
7
+ <%- if attribute.type == :boolean -%>
8
+ input(type: :checkbox)
9
+ output(type: :boolean, presence: true)
10
+ <%- elsif attribute.type == :text -%>
11
+ input(type: :textarea)
12
+ output(type: :string, presence: true)
13
+ <%- elsif attribute.type == :decimal || attribute.type == :float -%>
14
+ input(type: :number, step: 'any')
15
+ output(type: :decimal, presence: true)
16
+ <%- elsif attribute.type == :integer -%>
17
+ input(type: :number)
18
+ output(type: :integer, presence: true)
19
+ <%- elsif attribute.type == :date -%>
20
+ input(type: :date)
21
+ output(type: :date, presence: true)
22
+ <%- elsif attribute.type == :datetime || attribute.type == :timestamp -%>
23
+ input(type: :datetime_local)
24
+ output(type: :datetime, presence: true)
25
+ <%- else -%>
26
+ input(type: :text)
27
+ output(type: :string, presence: true)
28
+ <%- end -%>
29
+ end
30
+ <%- end -%>
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ module <%= controller_class_name %>
2
+ class SearchForm < ActionForm::Base
3
+ <%- simple_params.each do |attribute| -%>
4
+
5
+ element :<%= attribute.column_name %> do
6
+ <%- if attribute.type == :boolean -%>
7
+ input(type: :checkbox)
8
+ output(type: :boolean)
9
+ <%- elsif attribute.type == :text -%>
10
+ input(type: :textarea)
11
+ output(type: :string)
12
+ <%- elsif attribute.type == :decimal || attribute.type == :float -%>
13
+ input(type: :number, step: 'any')
14
+ output(type: :decimal)
15
+ <%- elsif attribute.type == :integer -%>
16
+ input(type: :number)
17
+ output(type: :integer)
18
+ <%- elsif attribute.type == :date -%>
19
+ input(type: :date)
20
+ output(type: :date)
21
+ <%- elsif attribute.type == :datetime || attribute.type == :timestamp -%>
22
+ input(type: :datetime_local)
23
+ output(type: :datetime)
24
+ <%- else -%>
25
+ input(type: :text)
26
+ output(type: :string)
27
+ <%- end -%>
28
+ end
29
+ <%- end -%>
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ class <%= controller_class_name %>::UpdateHandler < ApplicationHandler
2
+ form <%= controller_class_name %>::ModelForm
3
+
4
+ url_params do
5
+ integer :id, presence: true
6
+ end
7
+
8
+ finder :<%= file_name %>, -> { <%= singular_name.camelize %>.find_by(id: url_params.id) }, validate_existence: true
9
+
10
+ def on_validation_success
11
+ call if current_action.update?
12
+ destroy if current_action.destroy?
13
+ end
14
+
15
+ def call
16
+ <%= file_name %>.update(form_params.to_h)
17
+ end
18
+
19
+ def destroy
20
+ <%= file_name %>.destroy
21
+ end
22
+
23
+ def form_attributes
24
+ <%- if controller_class_path.empty? -%>
25
+ { model: <%= file_name %> }
26
+ <%- else -%>
27
+ { model: [<%= controller_class_path.map{|path| ":#{path}"}.join(', ') %>, <%= file_name %>] }
28
+ <%- end -%>
29
+ end
30
+ end
31
+
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SteelWheel
4
+ # Provides callbacks functionality for handlers
5
+ module Callbacks
6
+ NOOP = ->(o) { o }.freeze
7
+
8
+ def callbacks
9
+ @callbacks ||= {}
10
+ end
11
+
12
+ def failure(*statuses, &block)
13
+ return callbacks[:unprocessable_entity] = block if statuses.empty?
14
+
15
+ statuses.each do |status|
16
+ callbacks[status] = block
17
+ end
18
+ end
19
+
20
+ def success(&block)
21
+ callbacks[:success] = block
22
+ end
23
+
24
+ private
25
+
26
+ def success_callback
27
+ callbacks.fetch(:success, NOOP).call(self)
28
+ end
29
+
30
+ def failure_callback
31
+ callbacks.fetch(http_status, NOOP).call(self)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SteelWheel
4
+ # Provides parameter and form components
5
+ module Components
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ base.include(InstanceMethods)
9
+ base.singleton_class.attr_writer :url_params_definition, :form_definition
10
+ end
11
+
12
+ module ClassMethods # rubocop:disable Style/Documentation
13
+ def url_params_definition
14
+ @url_params_definition ||= Class.new(SteelWheel::Params)
15
+ end
16
+
17
+ def form_definition
18
+ @form_definition ||= Class.new(ActionForm::Rails::Base)
19
+ end
20
+
21
+ def url_params(klass = nil, &block)
22
+ self.url_params_definition = Class.new(klass) if klass
23
+ url_params_definition.class_exec(self, &block) if block
24
+ end
25
+
26
+ def form(klass = nil, &block)
27
+ self.form_definition = Class.new(klass) if klass
28
+ form_definition.class_eval(&block) if block
29
+ end
30
+ end
31
+
32
+ module InstanceMethods # rubocop:disable Style/Documentation
33
+ def url_params
34
+ @url_params ||= self.class.url_params_definition.new(input)
35
+ end
36
+
37
+ def form_params
38
+ @form_params ||= if @form_scope
39
+ self.class.form_definition.params_definition.schema[@form_scope].class.new(form_input)
40
+ else
41
+ self.class.form_definition.params_definition.new(form_input)
42
+ end
43
+ end
44
+
45
+ def form(attrs = form_attributes)
46
+ @form ||= self.class.form_definition.new(**attrs)
47
+ end
48
+
49
+ def form_attributes
50
+ raise SteelWheel::FormAttributesNotImplementedError
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SteelWheel
4
+ # Provides filtering functionality for handlers
5
+ module Filters
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ base.include(InstanceMethods)
9
+ end
10
+
11
+ module ClassMethods # rubocop:disable Style/Documentation
12
+ def filter(name, &definition)
13
+ define_method("filter_by_#{name}", &definition)
14
+ end
15
+
16
+ def filterable(name)
17
+ alias_method :"initial_#{name}_scope", name
18
+ define_method(name) do
19
+ apply_filters(send(:"initial_#{name}_scope"), form_params.to_h)
20
+ end
21
+ end
22
+ end
23
+
24
+ module InstanceMethods # rubocop:disable Style/Documentation
25
+ def apply_filters(scope, search_params)
26
+ search_params.each do |key, value|
27
+ filter_method = "filter_by_#{key}"
28
+ raise SteelWheel::FilterNotImplementedError, key unless respond_to?(filter_method)
29
+
30
+ scope = send(filter_method, scope, value) if value.present?
31
+ end
32
+ scope
33
+ end
34
+ end
35
+ end
36
+ end