action_handler 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []