reactive-actions 0.1.0.pre.alpha.1
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +714 -0
- data/Rakefile +15 -0
- data/app/assets/javascripts/reactive_actions.js +100 -0
- data/app/controllers/reactive_actions/application_controller.rb +10 -0
- data/app/controllers/reactive_actions/reactive_actions_controller.rb +183 -0
- data/config/initializers/reactive_actions_logger.rb +4 -0
- data/config/routes.rb +5 -0
- data/lib/generators/reactive_actions/install/install_generator.rb +261 -0
- data/lib/generators/reactive_actions/install/templates/README +32 -0
- data/lib/generators/reactive_actions/install/templates/example_action.rb +26 -0
- data/lib/generators/reactive_actions/install/templates/initializer.rb +13 -0
- data/lib/reactive-actions.rb +3 -0
- data/lib/reactive_actions/configuration.rb +33 -0
- data/lib/reactive_actions/engine.rb +64 -0
- data/lib/reactive_actions/errors.rb +24 -0
- data/lib/reactive_actions/reactive_action.rb +67 -0
- data/lib/reactive_actions/version.rb +5 -0
- data/lib/reactive_actions.rb +19 -0
- metadata +93 -0
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
|
5
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
6
|
+
load 'rails/tasks/engine.rake'
|
7
|
+
|
8
|
+
load 'rails/tasks/statistics.rake'
|
9
|
+
|
10
|
+
require 'bundler/gem_tasks'
|
11
|
+
|
12
|
+
require 'rspec/core/rake_task'
|
13
|
+
RSpec::Core::RakeTask.new(:spec)
|
14
|
+
|
15
|
+
task default: :spec
|
@@ -0,0 +1,100 @@
|
|
1
|
+
// ReactiveActions JavaScript Client for Rails 8
|
2
|
+
// ES Module compatible version
|
3
|
+
|
4
|
+
class ReactiveActionsClient {
|
5
|
+
constructor() {
|
6
|
+
this.baseUrl = '/reactive_actions/execute';
|
7
|
+
}
|
8
|
+
|
9
|
+
// Get CSRF token from meta tag
|
10
|
+
getCSRFToken() {
|
11
|
+
const metaTag = document.querySelector('meta[name="csrf-token"]');
|
12
|
+
return metaTag ? metaTag.getAttribute('content') : null;
|
13
|
+
}
|
14
|
+
|
15
|
+
// Execute an action with parameters
|
16
|
+
async execute(actionName, actionParams = {}, options = {}) {
|
17
|
+
const method = options.method || 'POST';
|
18
|
+
const contentType = options.contentType || 'application/json';
|
19
|
+
|
20
|
+
// Build request options
|
21
|
+
const requestOptions = {
|
22
|
+
method: method,
|
23
|
+
headers: {
|
24
|
+
'Content-Type': contentType,
|
25
|
+
'X-Requested-With': 'XMLHttpRequest'
|
26
|
+
},
|
27
|
+
credentials: 'same-origin'
|
28
|
+
};
|
29
|
+
|
30
|
+
// Add CSRF token if available
|
31
|
+
const csrfToken = this.getCSRFToken();
|
32
|
+
if (csrfToken) {
|
33
|
+
requestOptions.headers['X-CSRF-Token'] = csrfToken;
|
34
|
+
}
|
35
|
+
|
36
|
+
// Build URL or prepare body based on HTTP method
|
37
|
+
let url = this.baseUrl;
|
38
|
+
if (['GET', 'HEAD'].includes(method.toUpperCase())) {
|
39
|
+
// For GET requests, append parameters to URL
|
40
|
+
const params = new URLSearchParams({
|
41
|
+
action_name: actionName,
|
42
|
+
action_params: JSON.stringify(actionParams)
|
43
|
+
});
|
44
|
+
url = `${url}?${params.toString()}`;
|
45
|
+
} else {
|
46
|
+
// For other methods, send in request body
|
47
|
+
requestOptions.body = JSON.stringify({
|
48
|
+
action_name: actionName,
|
49
|
+
action_params: actionParams
|
50
|
+
});
|
51
|
+
}
|
52
|
+
|
53
|
+
try {
|
54
|
+
const response = await fetch(url, requestOptions);
|
55
|
+
const data = await response.json();
|
56
|
+
|
57
|
+
// Add response status and ok to the result
|
58
|
+
return {
|
59
|
+
...data,
|
60
|
+
status: response.status,
|
61
|
+
ok: response.ok
|
62
|
+
};
|
63
|
+
} catch (error) {
|
64
|
+
console.error('ReactiveActions error:', error);
|
65
|
+
throw error;
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
// Convenience methods for different HTTP verbs
|
70
|
+
async get(actionName, actionParams = {}, options = {}) {
|
71
|
+
return this.execute(actionName, actionParams, { ...options, method: 'GET' });
|
72
|
+
}
|
73
|
+
|
74
|
+
async post(actionName, actionParams = {}, options = {}) {
|
75
|
+
return this.execute(actionName, actionParams, { ...options, method: 'POST' });
|
76
|
+
}
|
77
|
+
|
78
|
+
async put(actionName, actionParams = {}, options = {}) {
|
79
|
+
return this.execute(actionName, actionParams, { ...options, method: 'PUT' });
|
80
|
+
}
|
81
|
+
|
82
|
+
async patch(actionName, actionParams = {}, options = {}) {
|
83
|
+
return this.execute(actionName, actionParams, { ...options, method: 'PATCH' });
|
84
|
+
}
|
85
|
+
|
86
|
+
async delete(actionName, actionParams = {}, options = {}) {
|
87
|
+
return this.execute(actionName, actionParams, { ...options, method: 'DELETE' });
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
// Create and export the instance
|
92
|
+
const ReactiveActions = new ReactiveActionsClient();
|
93
|
+
|
94
|
+
// For backward compatibility, also expose as global
|
95
|
+
if (typeof window !== 'undefined') {
|
96
|
+
window.ReactiveActions = ReactiveActions;
|
97
|
+
}
|
98
|
+
|
99
|
+
// ES Module export
|
100
|
+
export default ReactiveActions;
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveActions
|
4
|
+
# Base controller for the ReactiveActions engine
|
5
|
+
# Provides common functionality and settings for all controllers in the engine
|
6
|
+
class ApplicationController < ActionController::Base
|
7
|
+
# This controller is the base controller for all controllers in the engine
|
8
|
+
protect_from_forgery with: :exception
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReactiveActions
|
4
|
+
# Main controller for handling reactive action requests
|
5
|
+
# Processes action execution and manages error handling for the ReactiveActions engine
|
6
|
+
class ReactiveActionsController < ApplicationController
|
7
|
+
# Allow this action to handle any HTTP method
|
8
|
+
def execute
|
9
|
+
ReactiveActions.logger.info "ReactiveActionsController#execute[#{request.method}]: #{params.inspect}"
|
10
|
+
build_reactive_action.run
|
11
|
+
rescue ReactiveActions::Error => e
|
12
|
+
handle_reactive_actions_error(e)
|
13
|
+
rescue StandardError => e
|
14
|
+
handle_standard_error(e)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# Build and initialize the action
|
20
|
+
def build_reactive_action
|
21
|
+
initialize_action(extract_action_name, extract_action_params)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Extract and validate action_name parameter
|
25
|
+
def extract_action_name
|
26
|
+
action_name = params[:action_name]
|
27
|
+
raise ReactiveActions::MissingParameterError, 'Missing action_name parameter' if action_name.blank?
|
28
|
+
|
29
|
+
# Sanitize action_name to prevent code injection
|
30
|
+
sanitized_name = action_name.to_s.strip
|
31
|
+
raise ReactiveActions::InvalidParametersError, 'Invalid action_name format' unless sanitized_name.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
|
32
|
+
|
33
|
+
sanitized_name
|
34
|
+
end
|
35
|
+
|
36
|
+
# Extract and sanitize action_params with proper strong parameters
|
37
|
+
def extract_action_params
|
38
|
+
raw_params = params[:action_params]
|
39
|
+
return {} if raw_params.blank?
|
40
|
+
|
41
|
+
# For JSON requests, the params might already be a hash
|
42
|
+
if raw_params.is_a?(String)
|
43
|
+
begin
|
44
|
+
parsed_params = JSON.parse(raw_params)
|
45
|
+
permit_nested_params(parsed_params)
|
46
|
+
rescue JSON::ParserError
|
47
|
+
raise ReactiveActions::InvalidParametersError, 'Invalid JSON in action_params'
|
48
|
+
end
|
49
|
+
else
|
50
|
+
# Handle ActionController::Parameters, Hash, or any other object
|
51
|
+
permit_nested_params(raw_params)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Recursively permit nested parameters
|
56
|
+
# This allows for flexible parameter structures while maintaining security
|
57
|
+
def permit_nested_params(params)
|
58
|
+
case params
|
59
|
+
when ActionController::Parameters, Hash
|
60
|
+
permit_hash_params(params)
|
61
|
+
when Array
|
62
|
+
params.map { |item| permit_nested_params(item) }
|
63
|
+
else
|
64
|
+
sanitize_param_value(params)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Handle hash-like parameters (ActionController::Parameters or Hash)
|
69
|
+
def permit_hash_params(params)
|
70
|
+
permitted_hash = {}
|
71
|
+
params.each do |key, value|
|
72
|
+
sanitized_key = sanitize_param_key(key)
|
73
|
+
next if sanitized_key.nil?
|
74
|
+
|
75
|
+
permitted_hash[sanitized_key] = permit_nested_params(value)
|
76
|
+
end
|
77
|
+
permitted_hash
|
78
|
+
end
|
79
|
+
|
80
|
+
# Sanitize parameter keys to prevent injection attacks
|
81
|
+
def sanitize_param_key(key)
|
82
|
+
key_str = key.to_s.strip
|
83
|
+
|
84
|
+
# Only allow alphanumeric characters, underscores, and hyphens
|
85
|
+
return nil unless key_str.match?(/\A[a-zA-Z0-9_-]+\z/)
|
86
|
+
|
87
|
+
# Prevent keys that start with dangerous prefixes
|
88
|
+
dangerous_prefixes = %w[__ eval exec system `]
|
89
|
+
return nil if dangerous_prefixes.any? { |prefix| key_str.start_with?(prefix) }
|
90
|
+
|
91
|
+
key_str
|
92
|
+
end
|
93
|
+
|
94
|
+
# Sanitize parameter values based on type
|
95
|
+
def sanitize_param_value(value)
|
96
|
+
return value if safe_primitive_type?(value)
|
97
|
+
return truncate_string(value) if value.is_a?(String)
|
98
|
+
|
99
|
+
# For other types, preserve original value if reasonable
|
100
|
+
value.respond_to?(:to_s) && !value.is_a?(Object) ? truncate_string(value.to_s) : value
|
101
|
+
end
|
102
|
+
|
103
|
+
# Check if value is a safe primitive type that doesn't need sanitization
|
104
|
+
def safe_primitive_type?(value)
|
105
|
+
value.is_a?(Numeric) || value.is_a?(TrueClass) || value.is_a?(FalseClass) ||
|
106
|
+
value.nil? || value.is_a?(Time) || value.is_a?(Date) || value.is_a?(DateTime)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Truncate strings to prevent memory exhaustion
|
110
|
+
def truncate_string(string)
|
111
|
+
max_length = string == string.to_s ? 10_000 : 1_000
|
112
|
+
string.length > max_length ? string[0, max_length] : string
|
113
|
+
end
|
114
|
+
|
115
|
+
def handle_reactive_actions_error(exception)
|
116
|
+
error_mapping = {
|
117
|
+
'ActionNotFoundError' => [:not_found, 'NOT_FOUND'],
|
118
|
+
'MissingParameterError' => [:bad_request, 'MISSING_PARAMETER'],
|
119
|
+
'InvalidParametersError' => [:bad_request, 'INVALID_PARAMETERS'],
|
120
|
+
'UnauthorizedError' => [:forbidden, 'UNAUTHORIZED'],
|
121
|
+
'ActionExecutionError' => [:unprocessable_entity, 'EXECUTION_ERROR']
|
122
|
+
}
|
123
|
+
|
124
|
+
error_type = exception.class.name.demodulize
|
125
|
+
status, code = error_mapping[error_type]
|
126
|
+
|
127
|
+
ReactiveActions.logger.error "#{error_type}: #{exception.message}"
|
128
|
+
render_error(exception, status, code)
|
129
|
+
end
|
130
|
+
|
131
|
+
def handle_standard_error(exception)
|
132
|
+
ReactiveActions.logger.error "Unexpected error: #{exception.message}"
|
133
|
+
render_error(exception, :internal_server_error, 'SERVER_ERROR')
|
134
|
+
end
|
135
|
+
|
136
|
+
# Helper method to render standardized error responses
|
137
|
+
def render_error(exception, status, code)
|
138
|
+
render json: {
|
139
|
+
success: false,
|
140
|
+
error: {
|
141
|
+
type: exception.class.name.demodulize,
|
142
|
+
message: exception.message,
|
143
|
+
code: code
|
144
|
+
}
|
145
|
+
}, status: status
|
146
|
+
end
|
147
|
+
|
148
|
+
# Find and execute the requested action
|
149
|
+
def initialize_action(action_name, action_params)
|
150
|
+
# Convert snake_case action name to CamelCase class name
|
151
|
+
# e.g., "update_user" becomes "UpdateUserAction"
|
152
|
+
class_name = build_class_name(action_name)
|
153
|
+
|
154
|
+
# Find the action class - this will look in several places:
|
155
|
+
# 1. ReactiveActions::{ClassName} (e.g., ReactiveActions::UpdateUserAction)
|
156
|
+
# 2. {ClassName} (e.g., UpdateUserAction) in global namespace
|
157
|
+
action_class = find_action_class(class_name)
|
158
|
+
|
159
|
+
# Initialize the action and return it
|
160
|
+
# Add ** to convert hash to keyword arguments
|
161
|
+
action_class.new(self, **action_params.symbolize_keys)
|
162
|
+
end
|
163
|
+
|
164
|
+
def build_class_name(action_name)
|
165
|
+
class_name = action_name.to_s.camelize
|
166
|
+
# Add "Action" suffix if it doesn't already end with it
|
167
|
+
class_name.end_with?('Action') ? class_name : "#{class_name}Action"
|
168
|
+
end
|
169
|
+
|
170
|
+
# Find an action class using various lookup strategies
|
171
|
+
def find_action_class(class_name)
|
172
|
+
# First try within the ReactiveActions namespace
|
173
|
+
"ReactiveActions::#{class_name}".constantize
|
174
|
+
rescue NameError
|
175
|
+
begin
|
176
|
+
# Then try in the global namespace
|
177
|
+
class_name.constantize
|
178
|
+
rescue NameError
|
179
|
+
raise ReactiveActions::ActionNotFoundError, "Action '#{class_name.sub(/Action$/, '')}' not found"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module ReactiveActions
|
6
|
+
module Generators
|
7
|
+
# Interactive generator for installing ReactiveActions into a Rails application
|
8
|
+
# Prompts user for installation preferences and customization options
|
9
|
+
class InstallGenerator < Rails::Generators::Base
|
10
|
+
source_root File.expand_path('templates', __dir__)
|
11
|
+
|
12
|
+
desc 'Installs ReactiveActions with interactive configuration'
|
13
|
+
|
14
|
+
# Command line options
|
15
|
+
class_option :skip_routes, type: :boolean, default: false,
|
16
|
+
desc: 'Skip adding routes to the application'
|
17
|
+
class_option :skip_javascript, type: :boolean, default: false,
|
18
|
+
desc: 'Skip adding JavaScript imports'
|
19
|
+
class_option :skip_example, type: :boolean, default: false,
|
20
|
+
desc: 'Skip generating example action'
|
21
|
+
class_option :mount_path, type: :string, default: '/reactive_actions',
|
22
|
+
desc: 'Custom mount path for ReactiveActions'
|
23
|
+
class_option :quiet, type: :boolean, default: false,
|
24
|
+
desc: 'Run with minimal output'
|
25
|
+
|
26
|
+
def welcome_message
|
27
|
+
return if options[:quiet]
|
28
|
+
|
29
|
+
say 'Welcome to ReactiveActions installer!', :green
|
30
|
+
say 'This will help you set up ReactiveActions in your Rails application.'
|
31
|
+
say ''
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_initializer
|
35
|
+
if options[:quiet] || yes?('Create ReactiveActions initializer? (recommended)', :green)
|
36
|
+
template 'initializer.rb', 'config/initializers/reactive_actions.rb'
|
37
|
+
say '✓ Created initializer', :green unless options[:quiet]
|
38
|
+
else
|
39
|
+
say '✗ Skipped initializer creation', :yellow
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_actions_directory
|
44
|
+
if options[:quiet] || yes?('Create app/reactive_actions directory?', :green)
|
45
|
+
empty_directory 'app/reactive_actions'
|
46
|
+
say '✓ Created actions directory', :green unless options[:quiet]
|
47
|
+
|
48
|
+
ask_for_example_action unless options[:skip_example]
|
49
|
+
else
|
50
|
+
say '✗ Skipped actions directory creation', :yellow
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def configure_routes
|
55
|
+
return if options[:skip_routes]
|
56
|
+
|
57
|
+
mount_path = determine_mount_path
|
58
|
+
return if mount_path.nil?
|
59
|
+
|
60
|
+
route "mount ReactiveActions::Engine, at: '#{mount_path}'"
|
61
|
+
say "✓ Added route mounting ReactiveActions at #{mount_path}", :green unless options[:quiet]
|
62
|
+
end
|
63
|
+
|
64
|
+
def configure_javascript
|
65
|
+
return if options[:skip_javascript]
|
66
|
+
return unless javascript_needed?
|
67
|
+
|
68
|
+
if options[:quiet] || yes?('Add ReactiveActions JavaScript client?', :green)
|
69
|
+
add_javascript_to_importmap
|
70
|
+
say '✓ Added JavaScript client to importmap', :green unless options[:quiet]
|
71
|
+
else
|
72
|
+
say '✗ Skipped JavaScript configuration', :yellow
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def configuration_options
|
77
|
+
return if options[:quiet]
|
78
|
+
return unless yes?('Configure advanced options?', :green)
|
79
|
+
|
80
|
+
configure_delegated_methods
|
81
|
+
configure_logging
|
82
|
+
end
|
83
|
+
|
84
|
+
def installation_summary
|
85
|
+
return if options[:quiet]
|
86
|
+
|
87
|
+
say '', :green
|
88
|
+
say '=' * 60, :green
|
89
|
+
say 'ReactiveActions installation complete!', :bold
|
90
|
+
say '=' * 60, :green
|
91
|
+
|
92
|
+
show_usage_instructions
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def should_skip_example_action?
|
98
|
+
return true if options[:skip_example]
|
99
|
+
return true if !options[:quiet] && !yes?('Generate example action file?', :green)
|
100
|
+
|
101
|
+
false
|
102
|
+
end
|
103
|
+
|
104
|
+
def example_action_name
|
105
|
+
if options[:quiet]
|
106
|
+
'example'
|
107
|
+
else
|
108
|
+
ask('What should the example action be called?', default: 'example')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def sanitize_action_name(action_name)
|
113
|
+
sanitized = action_name.to_s.strip.underscore
|
114
|
+
sanitized = 'example' if sanitized.blank?
|
115
|
+
sanitized.end_with?('_action') ? sanitized : "#{sanitized}_action"
|
116
|
+
end
|
117
|
+
|
118
|
+
def determine_mount_path
|
119
|
+
return options[:mount_path] if options[:quiet]
|
120
|
+
return nil if options[:skip_routes]
|
121
|
+
|
122
|
+
if yes?('Add ReactiveActions routes to your application?', :green)
|
123
|
+
custom_path = ask('Mount path for ReactiveActions:', default: options[:mount_path])
|
124
|
+
sanitize_mount_path(custom_path)
|
125
|
+
else
|
126
|
+
say '✗ Skipped route configuration', :yellow
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def sanitize_mount_path(path)
|
132
|
+
sanitized = path.to_s.strip
|
133
|
+
sanitized = options[:mount_path] if sanitized.blank?
|
134
|
+
sanitized.start_with?('/') ? sanitized : "/#{sanitized}"
|
135
|
+
end
|
136
|
+
|
137
|
+
def javascript_needed?
|
138
|
+
using_importmap? || using_sprockets?
|
139
|
+
end
|
140
|
+
|
141
|
+
def using_importmap?
|
142
|
+
File.exist?('config/importmap.rb')
|
143
|
+
end
|
144
|
+
|
145
|
+
def using_sprockets?
|
146
|
+
File.exist?('app/assets/config/manifest.js')
|
147
|
+
end
|
148
|
+
|
149
|
+
def add_javascript_to_importmap
|
150
|
+
if using_importmap?
|
151
|
+
add_to_importmap
|
152
|
+
elsif using_sprockets?
|
153
|
+
add_to_sprockets_manifest
|
154
|
+
else
|
155
|
+
say 'No supported JavaScript setup detected (importmap or sprockets)', :yellow
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def add_to_importmap
|
160
|
+
importmap_content = <<~IMPORTMAP
|
161
|
+
|
162
|
+
# ReactiveActions JavaScript client
|
163
|
+
pin "reactive_actions", to: "reactive_actions.js"
|
164
|
+
IMPORTMAP
|
165
|
+
|
166
|
+
append_to_file 'config/importmap.rb', importmap_content
|
167
|
+
|
168
|
+
# Add the import to application.js to ensure it executes
|
169
|
+
add_import_to_application_js
|
170
|
+
end
|
171
|
+
|
172
|
+
def add_import_to_application_js
|
173
|
+
app_js_paths = %w[
|
174
|
+
app/javascript/application.js
|
175
|
+
app/assets/javascripts/application.js
|
176
|
+
app/javascript/controllers/application.js
|
177
|
+
]
|
178
|
+
|
179
|
+
app_js_path = app_js_paths.find { |path| File.exist?(path) }
|
180
|
+
|
181
|
+
if app_js_path
|
182
|
+
# Check if import already exists
|
183
|
+
app_js_content = File.read(app_js_path)
|
184
|
+
return if app_js_content.include?('import "reactive_actions"')
|
185
|
+
|
186
|
+
import_line = <<~JAVASCRIPT
|
187
|
+
|
188
|
+
// Import ReactiveActions to make it globally available
|
189
|
+
import "reactive_actions"
|
190
|
+
JAVASCRIPT
|
191
|
+
|
192
|
+
append_to_file app_js_path, import_line
|
193
|
+
say "✓ Added ReactiveActions import to #{app_js_path}", :green unless options[:quiet]
|
194
|
+
else
|
195
|
+
say '⚠ Could not find application.js file. Please manually add: import "reactive_actions"', :yellow
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def add_to_sprockets_manifest
|
200
|
+
return unless File.exist?('app/assets/config/manifest.js')
|
201
|
+
|
202
|
+
append_to_file 'app/assets/config/manifest.js' do
|
203
|
+
"\n//= link reactive_actions.js\n"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def configure_delegated_methods
|
208
|
+
return unless yes?('Add custom controller methods to delegate to actions?', :green)
|
209
|
+
|
210
|
+
methods = ask('Enter method names (comma-separated):')
|
211
|
+
return if methods.blank?
|
212
|
+
|
213
|
+
method_array = methods.split(',').map(&:strip).map(&:to_sym)
|
214
|
+
add_custom_config('delegated_controller_methods', method_array)
|
215
|
+
end
|
216
|
+
|
217
|
+
def configure_logging
|
218
|
+
log_level = ask('Set logging level:',
|
219
|
+
limited_to: %w[debug info warn error fatal],
|
220
|
+
default: 'info')
|
221
|
+
|
222
|
+
add_custom_config('log_level', log_level) unless log_level == 'info'
|
223
|
+
end
|
224
|
+
|
225
|
+
def add_custom_config(option, value)
|
226
|
+
initializer_path = 'config/initializers/reactive_actions.rb'
|
227
|
+
return unless File.exist?(initializer_path)
|
228
|
+
|
229
|
+
config_line = build_config_line(option, value)
|
230
|
+
append_to_file initializer_path, "\n#{config_line}\n"
|
231
|
+
end
|
232
|
+
|
233
|
+
def build_config_line(option, value)
|
234
|
+
case option
|
235
|
+
when 'delegated_controller_methods'
|
236
|
+
" config.delegated_controller_methods += #{value}"
|
237
|
+
when 'log_level'
|
238
|
+
"ReactiveActions.logger.level = :#{value}"
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def show_usage_instructions
|
243
|
+
if yes?('Show usage instructions?', :green)
|
244
|
+
readme 'README' if behavior == :invoke
|
245
|
+
else
|
246
|
+
say 'You can find usage instructions in the ReactiveActions documentation.'
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def ask_for_example_action
|
251
|
+
return if should_skip_example_action?
|
252
|
+
|
253
|
+
action_name = example_action_name
|
254
|
+
sanitized_name = sanitize_action_name(action_name)
|
255
|
+
|
256
|
+
template 'example_action.rb', "app/reactive_actions/#{sanitized_name}.rb"
|
257
|
+
say "✓ Created #{sanitized_name}.rb", :green unless options[:quiet]
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
===============================================================================
|
2
|
+
|
3
|
+
ReactiveActions has been installed successfully!
|
4
|
+
|
5
|
+
Routes have been added to your config/routes.rb file:
|
6
|
+
mount ReactiveActions::Engine, at: '/reactive_actions'
|
7
|
+
|
8
|
+
You can now access the reactive actions functionality at:
|
9
|
+
http://localhost:3000/reactive_actions/execute
|
10
|
+
|
11
|
+
A sample action has been created at:
|
12
|
+
app/reactive_actions/example_action.rb
|
13
|
+
|
14
|
+
You can try it out with:
|
15
|
+
|
16
|
+
# Using HTTP
|
17
|
+
curl -X POST http://localhost:3000/reactive_actions/execute \
|
18
|
+
-H "Content-Type: application/json" \
|
19
|
+
-d '{"action_name": "example", "action_params": {"name": "YourName"}}'
|
20
|
+
|
21
|
+
# Using JavaScript client
|
22
|
+
ReactiveActions.execute('example', { name: 'YourName' })
|
23
|
+
.then(response => console.log(response))
|
24
|
+
|
25
|
+
To create your own actions, add more files to the app/reactive_actions directory.
|
26
|
+
Each action should inherit from ReactiveActions::ReactiveAction and define
|
27
|
+
at least the `action` and `response` methods.
|
28
|
+
|
29
|
+
For more information, see the documentation at:
|
30
|
+
https://github.com/IstvanMs/reactive-actions
|
31
|
+
|
32
|
+
===============================================================================
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Example reactive action
|
4
|
+
# This action demonstrates the basic structure of a reactive action
|
5
|
+
class ExampleAction < ReactiveActions::ReactiveAction
|
6
|
+
# The main action logic goes here
|
7
|
+
def action
|
8
|
+
# You can access action parameters via the action_params
|
9
|
+
name = action_params[:name] || 'World'
|
10
|
+
|
11
|
+
# Generate a result hash (optional)
|
12
|
+
@result = {
|
13
|
+
status: :success,
|
14
|
+
message: "Hello, #{name}!"
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
# Define how to respond to the client
|
19
|
+
def response
|
20
|
+
# You can use controller methods like render
|
21
|
+
render json: {
|
22
|
+
success: true,
|
23
|
+
data: @result
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# ReactiveActions configuration
|
4
|
+
ReactiveActions.configure do |config|
|
5
|
+
# Configure methods to delegate from the controller to action classes
|
6
|
+
# config.delegated_controller_methods += [:custom_method]
|
7
|
+
|
8
|
+
# Configure instance variables to delegate from the controller to action classes
|
9
|
+
# config.delegated_instance_variables += [:custom_variable]
|
10
|
+
end
|
11
|
+
|
12
|
+
# Set the logger for ReactiveActions
|
13
|
+
ReactiveActions.logger = Rails.logger
|