action_handler 0.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2cb17f485ca9bcfe1a0d3f8ccf1273d5a11dc0ab00f8fc24e46c4696f2c85d41
4
+ data.tar.gz: 2a19b8f23d01be2202a3c4bc91797d25e94afeaa318e3c54f7f4f3c62ea2c2d6
5
+ SHA512:
6
+ metadata.gz: e24ba8bc134de0cdec88fba626866666439db82f928dcda58cc6626bed09785ff99b938d2934eccee81651bfe8569daf49bba85c8fb2196bacffccc4f46d6435
7
+ data.tar.gz: 9b5269ec69e02c98fbffa7e3221a5c89ad6699443e36a34f5d7747c26c84e5687e6695ffaaaf87a7053345d1666b3a2c96c5ebf9ea75056c5442a9523fcbc293
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 1970 ryym
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # Action Handler
2
+
3
+ [![CircleCI](https://circleci.com/gh/ryym/action_handler.svg?style=svg)](https://circleci.com/gh/ryym/action_handler)
4
+
5
+ ActionHandler is a Rails plugin that helps you write controller functionalities in clean and testable way.
6
+
7
+ ## What is a handler?
8
+
9
+ A handler is a controller-like class. Each public method can be an action method.
10
+ But unlike controllers, handlers inherit few methods by default.
11
+ Intead of using super class methods such as `params`, `sessions`, you can take them as arguments.
12
+ And you need to represent a response (data for views) as a single return value,
13
+ instead of assiging multiple instance variables.
14
+
15
+ ```ruby
16
+ # Example
17
+ class UsersHandler
18
+ include ActionHandler::Equip # (optional)
19
+
20
+ def initialize(ranking: RankingService.new)
21
+ @ranking = ranking
22
+ end
23
+
24
+ # Get request information via arguments.
25
+ def index(params, cookies)
26
+ puts "cookies: #{cookies}"
27
+
28
+ users = User.order(:id).page(params[:page])
29
+ ranks = @ranking.generate(users)
30
+
31
+ # Return a hash. It will be passed to controller's `render`.
32
+ { locals: { users: users, ranks: ranks } }
33
+ end
34
+
35
+ # Define custom argument.
36
+ arg(:user_id) do |ctrl|
37
+ ctrl.params[:id]
38
+ end
39
+
40
+ def show(user_id, format)
41
+ user = User.find(user_id)
42
+
43
+ # `render` is available as well. It just returns a given hash.
44
+ if format == :html
45
+ render locals: { user: user }
46
+ else
47
+ render json: user
48
+ end
49
+ end
50
+
51
+ # ...
52
+ end
53
+ ```
54
+
55
+ ## Features
56
+
57
+ ### Clean and clear structure
58
+
59
+ - In handlers, action methods take necessary inputs as arguments and
60
+ return output as a return value.
61
+ So easy to read and test.
62
+ - Handler is just a class, so you can set up any dependencies via `initialize` method.
63
+
64
+ ### Automatic arguments injection
65
+
66
+ - ActionHandler automatically injects common controller properties (`params`, `request`, etc)
67
+ just by declaring them as action method's arguments.
68
+ - You can define custom injectable arguments as well.
69
+
70
+ Note that this feature is heavily inspired by [ActionArgs](https://github.com/asakusarb/action_args).
71
+
72
+ ## Motivation
73
+
74
+ - Testability is important as much or more than test itself.
75
+ - Prefer clarity and simplicity over code shortness and easiness, for future maintainability.
76
+
77
+ ## Getting Started
78
+
79
+ Installation:
80
+
81
+ ```bash
82
+ gem install action_handler
83
+ ```
84
+
85
+ A simplest handler is a plain class with no super classes.
86
+
87
+ ```ruby
88
+ class UsersHandler
89
+ def index(params)
90
+ users = User.order(:id).page(params[:page])
91
+ { locals: { users: users } }
92
+ end
93
+
94
+ def show(params)
95
+ user = User.find(params[:id])
96
+ { locals: { user: user } }
97
+ end
98
+ end
99
+ ```
100
+
101
+ To use this handler, register it in a controller.
102
+
103
+ ```ruby
104
+ class UsersController < ApplicationController
105
+ include ActionHandler::Controller
106
+
107
+ use_handler { UsersHandler.new }
108
+ end
109
+ ```
110
+
111
+ This makes the controller implement same name action methods.
112
+ Now you can define routes to this controller as usual.
113
+
114
+ ```ruby
115
+ resources :users, only: %i[index show]
116
+ ```
117
+
118
+
119
+ Though you can use plain handler classes, usually you need to include `ActionHandler::Equip` module
120
+ to use basic controller functionalities like `redirect_to` or custom arguments.
121
+
122
+ ## Guides
123
+
124
+ See [Wiki][wiki] for detailed guides.
125
+
126
+ [wiki]: https://github.com/ryym/action_handler/wiki
127
+
128
+ TODO:
129
+
130
+ - Currently Unsupported controller features
131
+ - Where should handlers be placed?
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionHandler
4
+ module Args
5
+ # Args::Default is a default arguments supplier for handler methods.
6
+ class Default
7
+ %i[params request response cookies flash session logger].each do |key|
8
+ define_method(key) do |ctrl|
9
+ ctrl.send(key)
10
+ end
11
+ end
12
+
13
+ def reset_session(ctrl)
14
+ ctrl.method(:reset_session)
15
+ end
16
+
17
+ def format(ctrl)
18
+ ctrl.request.format
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionHandler
4
+ module Args
5
+ class Params
6
+ def initialize(*names, **nested_params)
7
+ names.each do |name|
8
+ define_singleton_method(name) do |ctrl|
9
+ ctrl.params[name]
10
+ end
11
+ end
12
+
13
+ nested_params.each do |name, fields|
14
+ define_singleton_method(name) do |ctrl|
15
+ ctrl.params.require(name).permit(*fields)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_handler/args/params'
4
+
5
+ module ActionHandler
6
+ module Args
7
+ module_function def from_hash(name_to_proc)
8
+ klass = Class.new do
9
+ name_to_proc.each do |name, proc|
10
+ define_method(name, &proc)
11
+ end
12
+ end
13
+
14
+ klass.new
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionHandler
4
+ class ArgsMaker
5
+ # TODO: Support optional arguments and keyword arguments.
6
+
7
+ def make_args(parameters, supplier, context: nil)
8
+ supplier_args = [context].compact
9
+ parameters.inject([]) do |values, (_, name)|
10
+ unless supplier.respond_to?(name)
11
+ raise ArgumentError, "parameter #{name} is not defined in #{supplier}"
12
+ end
13
+
14
+ values << supplier.send(name, *supplier_args)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionHandler
4
+ class Call
5
+ attr_reader :method_name
6
+ attr_reader :args
7
+
8
+ def initialize(method_name, args = [])
9
+ raise ArgumentError, 'args must be an array' unless args.is_a?(Array)
10
+
11
+ @method_name = method_name.to_sym
12
+ @args = args.freeze
13
+ end
14
+
15
+ def call_with(receiver)
16
+ receiver.send(method_name, *args)
17
+ end
18
+
19
+ # overrides
20
+ def ==(other)
21
+ other.is_a?(ActionHandler::Call) &&
22
+ method_name == other.method_name &&
23
+ args == other.args
24
+ end
25
+
26
+ # overrides
27
+ def hash
28
+ [method_name, args].hash
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionHandler
4
+ CONFIG_VAR_NAME = :@_action_handler_config
5
+
6
+ class Config
7
+ def self.get(handler_class)
8
+ handler_class.instance_variable_get(CONFIG_VAR_NAME)
9
+ end
10
+
11
+ def self.set(handler_class, config)
12
+ raise ArgumentError, 'invalid config' unless config.is_a?(self)
13
+
14
+ handler_class.instance_variable_set(CONFIG_VAR_NAME, config)
15
+ end
16
+
17
+ attr_reader :as_controller
18
+ attr_reader :action_methods
19
+ attr_reader :args_suppliers
20
+ attr_reader :custom_args
21
+
22
+ def initialize
23
+ @as_controller = nil
24
+ @action_methods = nil
25
+ @args_suppliers = []
26
+ @custom_args = {} # { method_name: proc }
27
+ end
28
+
29
+ def as_controller=(block)
30
+ raise ArgumentError, 'must be proc' unless block.is_a?(Proc)
31
+
32
+ @as_controller = block
33
+ end
34
+
35
+ def action_methods=(names)
36
+ raise ArgumentError, 'must be array' unless names.is_a?(Array)
37
+
38
+ @action_methods = names
39
+ end
40
+
41
+ def add_args_supplier(supplier)
42
+ @args_suppliers.push(supplier)
43
+ end
44
+
45
+ def add_arg(name, &block)
46
+ @custom_args[name] = block
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_handler/installer'
4
+
5
+ module ActionHandler
6
+ module Controller
7
+ def self.included(ctrl_class)
8
+ ctrl_class.extend ActionHandler::ControllerExtension
9
+ end
10
+ end
11
+
12
+ module ControllerExtension
13
+ def use_handler
14
+ handler = yield
15
+ ActionHandler::Installer.new.install(handler, self)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_handler/call'
4
+ require 'action_handler/args/default'
5
+
6
+ module ActionHandler
7
+ # Equip implements most basic functionality of controller for handler.
8
+ module Equip
9
+ # Return the argument as is, because the actual rendering
10
+ # is done in a controller. This just makes returning values more easy.
11
+ # Without this:
12
+ #
13
+ # { status: :ok, json: user.to_json }
14
+ #
15
+ # With this (no braces):
16
+ #
17
+ # render status: :ok, json: user.to_json
18
+ def render(props)
19
+ props
20
+ end
21
+
22
+ def redirect_to(*args)
23
+ ActionHandler::Call.new(:redirect_to, args)
24
+ end
25
+
26
+ def urls
27
+ Rails.application.routes.url_helpers
28
+ end
29
+
30
+ def self.included(handler_class)
31
+ ActionHandler::Config.set(handler_class, ActionHandler::Config.new)
32
+ handler_class.extend ActionHandler::HandlerExtension
33
+ handler_class.args ActionHandler::Args::Default.new
34
+ end
35
+ end
36
+
37
+ module HandlerExtension
38
+ def as_controller(&block)
39
+ ActionHandler::Config.get(self).as_controller = block
40
+ end
41
+
42
+ def action_methods(*method_names)
43
+ ActionHandler::Config.get(self).action_methods = method_names
44
+ end
45
+
46
+ def args(*suppliers)
47
+ raise '`args` does not accept block. Use `arg` to define custom argument' if block_given?
48
+
49
+ config = ActionHandler::Config.get(self)
50
+ suppliers.each do |supplier|
51
+ config.add_args_supplier(supplier)
52
+ end
53
+ end
54
+
55
+ def args_params(*names)
56
+ ActionHandler::Config.get(self).add_args_supplier(
57
+ ActionHandler::Args::Params.new(*names),
58
+ )
59
+ end
60
+
61
+ def arg(name, &block)
62
+ unless block_given?
63
+ raise '`arg` requires block. Use `args` to register arguments supplier object'
64
+ end
65
+
66
+ ActionHandler::Config.get(self).add_arg(name, &block)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_handler/args_maker'
4
+ require 'action_handler/response_evaluator'
5
+
6
+ module ActionHandler
7
+ class Installer
8
+ attr_reader :args_maker
9
+ attr_reader :res_evaluator
10
+
11
+ def initialize(
12
+ args_maker: ActionHandler::ArgsMaker.new,
13
+ res_evaluator: ActionHandler::ResponseEvaluator.new
14
+ )
15
+ @args_maker = args_maker
16
+ @res_evaluator = res_evaluator
17
+ end
18
+
19
+ def install(handler, ctrl_class)
20
+ config = ActionHandler::Config.get(handler.class) || ActionHandler::Config.new
21
+
22
+ ctrl_class.class_eval(&config.as_controller) if config.as_controller
23
+
24
+ actions = action_methods(handler, config)
25
+ args_supplier = args_supplier(config)
26
+
27
+ actions.each do |name|
28
+ installer = self
29
+ ctrl_class.send(:define_method, name) do
30
+ method = handler.method(name)
31
+ args = installer.args_maker.make_args(
32
+ method.parameters,
33
+ args_supplier,
34
+ context: self,
35
+ )
36
+ res = method.call(*args)
37
+ installer.res_evaluator.evaluate(self, res)
38
+ end
39
+ end
40
+ end
41
+
42
+ private def action_methods(handler, config)
43
+ config.action_methods || own_public_methods(handler)
44
+ end
45
+
46
+ private def args_supplier(config)
47
+ args_hash = {}
48
+
49
+ config.args_suppliers.each do |supplier|
50
+ own_public_methods(supplier).each do |name|
51
+ args_hash[name] = supplier.method(name)
52
+ end
53
+ end
54
+
55
+ args_hash = args_hash.merge(config.custom_args)
56
+ ActionHandler::Args.from_hash(args_hash)
57
+ end
58
+
59
+ # List all public methods except super class methods.
60
+ private def own_public_methods(obj)
61
+ methods = obj.public_methods
62
+ obj.class.ancestors.drop(1).inject(methods) do |ms, sp|
63
+ ms - sp.instance_methods
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionHandler
4
+ # ResponseEvaluator evaluates and converts handler return values.
5
+ class ResponseEvaluator
6
+ def evaluate(ctrl, res)
7
+ case res
8
+ when Hash
9
+ ctrl.render(res)
10
+ when ActionHandler::Call
11
+ res.call_with(ctrl)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionHandler
4
+ VERSION = '0.0.0'
5
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_handler/config'
4
+ require 'action_handler/args'
5
+ require 'action_handler/controller'
6
+ require 'action_handler/equip'
7
+
8
+ module ActionHandler
9
+ # Enable to autoload handlers defined in a controller file.
10
+ # Rails autoloading works only if the constant is defined in
11
+ # a file matching its name. So if `FooHandler` is defined in
12
+ # `foo_controller.rb`, it cannot be autoloaded.
13
+ # (https://guides.rubyonrails.org/autoloading_and_reloading_constants.html)
14
+ #
15
+ # So this hooks const_missing and load the corresponding controller.
16
+ # If the controller exists, its handler will be loaded as well.
17
+ #
18
+ # Currently this supports only the handlers defined in the top level scope.
19
+ module_function def autoload_handlers_from_controller_file
20
+ unless defined? Rails
21
+ raise 'Rails is not defined. This method is supposed to use in Rails environment.'
22
+ end
23
+
24
+ return if @hook_registered
25
+
26
+ @hook_registered = true
27
+
28
+ # Perhaps this warning is a RuboCop's bug.
29
+ # rubocop:disable Lint/NestedMethodDefinition
30
+ def Object.const_missing(name)
31
+ # rubocop:enable Lint/NestedMethodDefinition
32
+
33
+ return super unless name =~ /\A[a-zA-Z0-9_]+Handler\z/
34
+ return super if name == :ActionHandler
35
+
36
+ # Try to autoload the corresponding controller.
37
+ prefix = name.to_s.sub(/Handler\z/, '')
38
+ begin
39
+ const_get("::#{prefix}Controller")
40
+ rescue NameError
41
+ super
42
+ end
43
+
44
+ # Return the handler if loaded.
45
+ return const_get(name) if Object.const_defined?(name)
46
+
47
+ # Otherwise raise the NameError.
48
+ super
49
+ end
50
+ end
51
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: action_handler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - ryym
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-11-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec_junit_formatter
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.60'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.60'
55
+ description: Makes your controllers more unit-testable.
56
+ email:
57
+ - ryym.64@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - MIT-LICENSE
63
+ - README.md
64
+ - lib/action_handler.rb
65
+ - lib/action_handler/args.rb
66
+ - lib/action_handler/args/default.rb
67
+ - lib/action_handler/args/params.rb
68
+ - lib/action_handler/args_maker.rb
69
+ - lib/action_handler/call.rb
70
+ - lib/action_handler/config.rb
71
+ - lib/action_handler/controller.rb
72
+ - lib/action_handler/equip.rb
73
+ - lib/action_handler/installer.rb
74
+ - lib/action_handler/response_evaluator.rb
75
+ - lib/action_handler/version.rb
76
+ homepage: https://github.com/ryym/action_handler
77
+ licenses:
78
+ - MIT
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.7.6
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Rails controller alternative
100
+ test_files: []