hanami-view 1.3.1 → 2.0.0.alpha3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE +20 -0
  4. data/README.md +17 -835
  5. data/hanami-view.gemspec +26 -16
  6. data/lib/hanami/view/application_configuration.rb +77 -0
  7. data/lib/hanami/view/application_context.rb +35 -0
  8. data/lib/hanami/view/application_view.rb +89 -0
  9. data/lib/hanami/view/context.rb +97 -0
  10. data/lib/hanami/view/context_helpers/content_helpers.rb +26 -0
  11. data/lib/hanami/view/decorated_attributes.rb +82 -0
  12. data/lib/hanami/view/errors.rb +31 -53
  13. data/lib/hanami/view/exposure.rb +126 -0
  14. data/lib/hanami/view/exposures.rb +74 -0
  15. data/lib/hanami/view/part.rb +217 -0
  16. data/lib/hanami/view/part_builder.rb +140 -0
  17. data/lib/hanami/view/path.rb +68 -0
  18. data/lib/hanami/view/render_environment.rb +62 -0
  19. data/lib/hanami/view/render_environment_missing.rb +44 -0
  20. data/lib/hanami/view/rendered.rb +55 -0
  21. data/lib/hanami/view/renderer.rb +79 -0
  22. data/lib/hanami/view/scope.rb +189 -0
  23. data/lib/hanami/view/scope_builder.rb +98 -0
  24. data/lib/hanami/view/standalone_view.rb +400 -0
  25. data/lib/hanami/view/tilt/erb.rb +26 -0
  26. data/lib/hanami/view/tilt/erbse.rb +21 -0
  27. data/lib/hanami/view/tilt/haml.rb +26 -0
  28. data/lib/hanami/view/tilt.rb +78 -0
  29. data/lib/hanami/view/version.rb +5 -5
  30. data/lib/hanami/view.rb +208 -223
  31. data/lib/hanami-view.rb +3 -1
  32. metadata +120 -70
  33. data/LICENSE.md +0 -22
  34. data/lib/hanami/layout.rb +0 -190
  35. data/lib/hanami/presenter.rb +0 -98
  36. data/lib/hanami/view/configuration.rb +0 -504
  37. data/lib/hanami/view/dsl.rb +0 -347
  38. data/lib/hanami/view/escape.rb +0 -225
  39. data/lib/hanami/view/inheritable.rb +0 -54
  40. data/lib/hanami/view/rendering/layout_finder.rb +0 -128
  41. data/lib/hanami/view/rendering/layout_registry.rb +0 -69
  42. data/lib/hanami/view/rendering/layout_scope.rb +0 -274
  43. data/lib/hanami/view/rendering/null_layout.rb +0 -52
  44. data/lib/hanami/view/rendering/null_local.rb +0 -82
  45. data/lib/hanami/view/rendering/null_template.rb +0 -83
  46. data/lib/hanami/view/rendering/null_view.rb +0 -26
  47. data/lib/hanami/view/rendering/options.rb +0 -24
  48. data/lib/hanami/view/rendering/partial.rb +0 -31
  49. data/lib/hanami/view/rendering/partial_file.rb +0 -29
  50. data/lib/hanami/view/rendering/partial_finder.rb +0 -75
  51. data/lib/hanami/view/rendering/partial_templates_finder.rb +0 -73
  52. data/lib/hanami/view/rendering/registry.rb +0 -134
  53. data/lib/hanami/view/rendering/scope.rb +0 -108
  54. data/lib/hanami/view/rendering/subscope.rb +0 -56
  55. data/lib/hanami/view/rendering/template.rb +0 -69
  56. data/lib/hanami/view/rendering/template_finder.rb +0 -55
  57. data/lib/hanami/view/rendering/template_name.rb +0 -50
  58. data/lib/hanami/view/rendering/templates_finder.rb +0 -144
  59. data/lib/hanami/view/rendering/view_finder.rb +0 -37
  60. data/lib/hanami/view/rendering.rb +0 -294
  61. data/lib/hanami/view/template.rb +0 -57
data/hanami-view.gemspec CHANGED
@@ -1,28 +1,38 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'hanami/view/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
8
  spec.name = 'hanami-view'
8
- spec.version = Hanami::View::VERSION
9
- spec.authors = ['Luca Guidi']
10
- spec.email = ['me@lucaguidi.com']
11
- spec.description = %q{View layer for Hanami}
12
- spec.summary = %q{View layer for Hanami, with a separation between views and templates}
13
- spec.homepage = 'http://hanamirb.org'
9
+ spec.authors = ["Tim Riley", "Piotr Solnica"]
10
+ spec.email = ["tim@icelab.com.au", "piotr.solnica@gmail.com"]
14
11
  spec.license = 'MIT'
12
+ spec.version = Hanami::View::VERSION.dup
15
13
 
16
- spec.files = `git ls-files -- lib/* CHANGELOG.md LICENSE.md README.md hanami-view.gemspec`.split($/)
14
+ spec.summary = "A complete, standalone view rendering system that gives you everything you need to write well-factored view code"
15
+ spec.description = spec.summary
16
+ spec.homepage = 'https://dry-rb.org/gems/hanami-view'
17
+ spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "hanami-view.gemspec", "lib/**/*"]
18
+ spec.bindir = 'bin'
17
19
  spec.executables = []
18
- spec.test_files = spec.files.grep(%r{^(test)/})
19
20
  spec.require_paths = ['lib']
20
- spec.required_ruby_version = '>= 2.3.0'
21
21
 
22
- spec.add_runtime_dependency 'tilt', '~> 2.0', '>= 2.0.1'
23
- spec.add_runtime_dependency 'hanami-utils', '~> 1.3'
22
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
23
+ spec.metadata['changelog_uri'] = 'https://github.com/hanami/view/blob/main/CHANGELOG.md'
24
+ spec.metadata['source_code_uri'] = 'https://github.com/hanami/view'
25
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/hanami/view/issues'
26
+
27
+ spec.required_ruby_version = ">= 2.4.0"
28
+
29
+ spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
30
+ spec.add_runtime_dependency "dry-configurable", "~> 0.13", ">= 0.13.0"
31
+ spec.add_runtime_dependency "dry-core", "~> 0.5", ">= 0.5"
32
+ spec.add_runtime_dependency "dry-inflector", "~> 0.1"
33
+ spec.add_runtime_dependency "tilt", "~> 2.0", ">= 2.0.6"
24
34
 
25
- spec.add_development_dependency 'bundler', '>= 1.6', '< 3'
26
- spec.add_development_dependency 'rspec', '~> 3.7'
27
- spec.add_development_dependency 'rake', '~> 12'
35
+ spec.add_development_dependency "bundler"
36
+ spec.add_development_dependency "rake"
37
+ spec.add_development_dependency "rspec"
28
38
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+ require_relative "../view"
5
+
6
+ module Hanami
7
+ class View
8
+ class ApplicationConfiguration
9
+ include Dry::Configurable
10
+
11
+ setting :parts_path, default: "views/parts"
12
+
13
+ def initialize(*)
14
+ super
15
+
16
+ @base_config = View.config.dup
17
+
18
+ configure_defaults
19
+ end
20
+
21
+ # Returns the list of available settings
22
+ #
23
+ # @return [Set]
24
+ #
25
+ # @since 2.0.0
26
+ # @api private
27
+ def settings
28
+ self.class.settings + View.settings - NON_FORWARDABLE_METHODS
29
+ end
30
+
31
+ def finalize!
32
+ return self if frozen?
33
+
34
+ base_config.finalize!
35
+
36
+ super
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :base_config
42
+
43
+ def configure_defaults
44
+ self.paths = ["web/templates"]
45
+ self.template_inference_base = "views"
46
+ self.layout = "application"
47
+ end
48
+
49
+ # An inflector for views is not configurable via `config.views.inflector` on an
50
+ # `Hanami::Application`. The application-wide inflector is already configurable
51
+ # there as `config.inflector` and will be used as the default inflector for views.
52
+ #
53
+ # A custom inflector may still be provided in an `Hanami::View` subclass, via
54
+ # `config.inflector=`.
55
+ NON_FORWARDABLE_METHODS = [:inflector, :inflector=].freeze
56
+ private_constant :NON_FORWARDABLE_METHODS
57
+
58
+ def method_missing(name, *args, &block)
59
+ return super if NON_FORWARDABLE_METHODS.include?(name)
60
+
61
+ if config.respond_to?(name)
62
+ config.public_send(name, *args, &block)
63
+ elsif base_config.respond_to?(name)
64
+ base_config.public_send(name, *args, &block)
65
+ else
66
+ super
67
+ end
68
+ end
69
+
70
+ def respond_to_missing?(name, _include_all = false)
71
+ return false if NON_FORWARDABLE_METHODS.include?(name)
72
+
73
+ config.respond_to?(name) || base_config.respond_to?(name) || super
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ class View
5
+ module ApplicationContext
6
+ def initialize(inflector: Hanami.application.inflector, **options)
7
+ @inflector = inflector
8
+ super
9
+ end
10
+
11
+ def inflector
12
+ @inflector
13
+ end
14
+
15
+ def request
16
+ _options.fetch(:request)
17
+ end
18
+
19
+ def session
20
+ request.session
21
+ end
22
+
23
+ def flash
24
+ response.flash
25
+ end
26
+
27
+ private
28
+
29
+ # TODO: create `Request#flash` so we no longer need the `response`
30
+ def response
31
+ _options.fetch(:response)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,89 @@
1
+ module Hanami
2
+ class View
3
+ class ApplicationView < Module
4
+ InheritedHook = Class.new(Module)
5
+
6
+ attr_reader :provider
7
+ attr_reader :application
8
+ attr_reader :inherited_hook
9
+
10
+ def initialize(provider)
11
+ @provider = provider
12
+ @application = provider.respond_to?(:application) ? provider.application : Hanami.application
13
+ @inherited_hook = InheritedHook.new
14
+
15
+ define_inherited_hook
16
+ end
17
+
18
+ def included(view_class)
19
+ configure_view view_class
20
+ view_class.extend inherited_hook
21
+ end
22
+
23
+ private
24
+
25
+ def configure_view(view_class)
26
+ view_class.settings.each do |setting|
27
+ if application.config.views.respond_to?(:"#{setting}")
28
+ application_value = application.config.views.public_send(:"#{setting}")
29
+ view_class.config.public_send :"#{setting}=", application_value
30
+ end
31
+ end
32
+
33
+ view_class.config.inflector = provider.inflector
34
+ view_class.config.paths = prepare_paths(provider, view_class.config.paths)
35
+ view_class.config.template = template_name(view_class)
36
+
37
+ if (part_namespace = namespace_from_path(application.config.views.parts_path))
38
+ view_class.config.part_namespace = part_namespace
39
+ end
40
+ end
41
+
42
+ def define_inherited_hook
43
+ template_name = method(:template_name)
44
+
45
+ inherited_hook.send :define_method, :inherited do |subclass|
46
+ super(subclass)
47
+ subclass.config.template = template_name.(subclass)
48
+ end
49
+ end
50
+
51
+ def prepare_paths(provider, configured_paths)
52
+ configured_paths.map { |path|
53
+ if path.dir.relative?
54
+ provider.root.join(path.dir)
55
+ else
56
+ path
57
+ end
58
+ }
59
+ end
60
+
61
+ def template_name(view_class)
62
+ provider
63
+ .inflector
64
+ .underscore(view_class.name)
65
+ .sub(/^#{provider.namespace_path}\//, "")
66
+ .sub(/^#{view_class.config.template_inference_base}\//, "")
67
+ end
68
+
69
+ def namespace_from_path(path)
70
+ path = "#{provider.namespace_path}/#{path}"
71
+
72
+ begin
73
+ require path
74
+ rescue LoadError => exception
75
+ raise exception unless exception.path == path
76
+ end
77
+
78
+ begin
79
+ inflector.constantize(inflector.camelize(path))
80
+ rescue NameError => exception
81
+ end
82
+ end
83
+
84
+ def inflector
85
+ provider.inflector
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+ require_relative "application_context"
5
+ require_relative "decorated_attributes"
6
+
7
+ module Hanami
8
+ class View
9
+ # Provides a baseline environment across all the templates, parts and scopes
10
+ # in a given rendering.
11
+ #
12
+ # @abstract Subclass this and add your own methods (along with a custom
13
+ # `#initialize` if you wish to inject dependencies)
14
+ #
15
+ # @api public
16
+ class Context
17
+ include Dry::Equalizer(:_options)
18
+ include DecoratedAttributes
19
+
20
+ attr_reader :_render_env, :_options
21
+
22
+ def self.inherited(subclass)
23
+ super
24
+
25
+ # When inheriting within an Hanami app, add application context behavior
26
+ if application_provider(subclass)
27
+ subclass.include ApplicationContext
28
+ end
29
+ end
30
+
31
+ def self.application_provider(subclass)
32
+ if Hanami.respond_to?(:application?) && Hanami.application?
33
+ Hanami.application.component_provider(subclass)
34
+ end
35
+ end
36
+ private_class_method :application_provider
37
+
38
+ # Returns a new instance of Context
39
+ #
40
+ # In subclasses, you should include an `**options` parameter and pass _all
41
+ # arguments_ to `super`. This allows Context to make copies of itself
42
+ # while preserving your dependencies.
43
+ #
44
+ # @example
45
+ # class MyContext < Hanami::View::Context
46
+ # # Injected dependency
47
+ # attr_reader :assets
48
+ #
49
+ # def initialize(assets:, **options)
50
+ # @assets = assets
51
+ # super
52
+ # end
53
+ # end
54
+ #
55
+ # @api public
56
+ def initialize(render_env: nil, **options)
57
+ @_render_env = render_env
58
+ @_options = options
59
+ end
60
+
61
+ # @api private
62
+ def for_render_env(render_env)
63
+ return self if render_env == _render_env
64
+
65
+ self.class.new(**_options.merge(render_env: render_env))
66
+ end
67
+
68
+ # Returns a copy of the Context with new options merged in.
69
+ #
70
+ # This may be useful to supply values for dependencies that are _optional_
71
+ # when initializing your custom Context subclass.
72
+ #
73
+ # @example
74
+ # class MyContext < Hanami::View::Context
75
+ # # Injected dependencies (request is optional)
76
+ # attr_reader :assets, :request
77
+ #
78
+ # def initialize(assets:, request: nil, **options)
79
+ # @assets = assets
80
+ # @request = reuqest
81
+ # super
82
+ # end
83
+ # end
84
+ #
85
+ # my_context = MyContext.new(assets: assets)
86
+ # my_context_with_request = my_context.with(request: request)
87
+ #
88
+ # @api public
89
+ def with(**new_options)
90
+ self.class.new(
91
+ render_env: _render_env,
92
+ **_options.merge(new_options)
93
+ )
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,26 @@
1
+ module Hanami
2
+ class View
3
+ module ContextHelpers
4
+ module ContentHelpers
5
+ def initialize(content: {}, **options)
6
+ super
7
+ end
8
+
9
+ def content_for(key, value = nil, &block)
10
+ content = _options[:content]
11
+ output = nil
12
+
13
+ if block
14
+ content[key] = yield
15
+ elsif value
16
+ content[key] = value
17
+ else
18
+ output = content[key]
19
+ end
20
+
21
+ output
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Hanami
6
+ class View
7
+ # Decorates attributes in Parts.
8
+ module DecoratedAttributes
9
+ # @api private
10
+ def self.included(klass)
11
+ klass.extend ClassInterface
12
+ end
13
+
14
+ # Decorated attributes class-level interface.
15
+ module ClassInterface
16
+ # @api private
17
+ MODULE_NAME = :DecoratedAttributes
18
+
19
+ # Decorates the provided attributes, wrapping them in Parts using the
20
+ # current render environment.
21
+ #
22
+ # @example
23
+ # class Article < Hanami::View::Part
24
+ # decorate :feature_image
25
+ # decorate :author as: :person
26
+ # end
27
+ #
28
+ # @param names [Array<Symbol>] the attribute names
29
+ # @param options [Hash] the options to pass to the Part Builder
30
+ # @option options [Symbol, Class] :as an alternative name or class to use when finding a
31
+ # matching Part
32
+ #
33
+ # @api public
34
+ def decorate(*names, **options)
35
+ decorated_attributes.decorate(*names, **options)
36
+ end
37
+
38
+ private
39
+
40
+ def decorated_attributes
41
+ if const_defined?(MODULE_NAME, false)
42
+ const_get(MODULE_NAME)
43
+ else
44
+ const_set(MODULE_NAME, Attributes.new).tap do |mod|
45
+ prepend mod
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ # @api private
52
+ class Attributes < Module
53
+ def initialize(*)
54
+ @names = Set.new
55
+ super
56
+ end
57
+
58
+ def decorate(*names, **options)
59
+ @names += names
60
+
61
+ class_eval do
62
+ names.each do |name|
63
+ define_method name do
64
+ attribute = super()
65
+
66
+ if _render_env && attribute
67
+ _render_env.part(name, attribute, **options)
68
+ else
69
+ attribute
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def inspect
77
+ %(#<#{self.class.name}#{@names.to_a.sort.inspect}>)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,65 +1,43 @@
1
- module Hanami
2
- module View
3
- # @since 0.5.0
4
- class Error < ::StandardError
5
- end
1
+ # frozen_string_literal: true
6
2
 
7
- # Missing template error
8
- #
9
- # This is raised at the runtime when Hanami::View cannot find a template for
10
- # the requested format.
11
- #
12
- # We can't raise this error during the loading phase, because at that time
13
- # we don't know if a view implements its own rendering policy.
14
- # A view is allowed to override `#render`, and this scenario can make the
15
- # presence of a template useless. One typical example is the usage of a
16
- # serializer that returns the output string, without rendering a template.
3
+ module Hanami
4
+ class View
5
+ # Error raised when critical settings are not configured
17
6
  #
18
- # @since 0.1.0
19
- class MissingTemplateError < Error
20
- # @since 0.1.0
21
- # @api private
22
- def initialize(template, format)
23
- super("Can't find template '#{ template }' for '#{ format }' format.")
7
+ # @api private
8
+ class UndefinedConfigError < StandardError
9
+ def initialize(key)
10
+ super("no +#{key}+ configured")
24
11
  end
25
12
  end
26
13
 
27
- # Missing format error
28
- #
29
- # This is raised at the runtime when rendering context lacks the :format
30
- # key.
31
- #
32
- # @since 0.1.0
33
- #
34
- # @see Hanami::View::Rendering#render
35
- class MissingFormatError < Error
36
- end
14
+ # Error raised when template could not be found within a view's configured
15
+ # paths
16
+ #
17
+ # @api private
18
+ class TemplateNotFoundError < StandardError
19
+ def initialize(template_name, lookup_paths)
20
+ msg = [
21
+ "Template +#{template_name}+ could not be found in paths:",
22
+ lookup_paths.map { |path| " - #{path}" }
23
+ ].join("\n\n")
37
24
 
38
- # Missing template layout error
39
- #
40
- # This is raised at the runtime when Hanami::Layout cannot find its template.
41
- #
42
- # @since 0.5.0
43
- class MissingTemplateLayoutError < Error
44
- # @since 0.5.0
45
- # @api private
46
- def initialize(template)
47
- super("Can't find layout template '#{ template }'")
25
+ super(msg)
48
26
  end
49
27
  end
50
28
 
51
- # Unknown or render type
52
- #
53
- # This is raised at the runtime when `Hanami::Layout` doesn't recognize the render type.
54
- #
55
- # @since 1.1.0
56
- class UnknownRenderTypeError < Error
57
- # @since 1.1.0
58
- # @api private
59
- def initialize(known_types, supplied_options)
60
- known_types_list = known_types.map{|t| "':#{t}'"}.join(', ')
61
- supplied_options_list = supplied_options.keys.map{|t| "':#{t}'"}.join(', ')
62
- super("Calls to `render` in a layout must include one of #{known_types_list}. Found #{supplied_options_list}.")
29
+ # Error raised when layout could not be found within a view's configured
30
+ # paths
31
+ #
32
+ # @api private
33
+ class LayoutNotFoundError < StandardError
34
+ def initialize(layout_name, lookup_paths)
35
+ msg = [
36
+ "Layout +#{layout_name}+ could not be found in paths:",
37
+ lookup_paths.map { |path| " - #{path}" }
38
+ ].join("\n\n")
39
+
40
+ super(msg)
63
41
  end
64
42
  end
65
43
  end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+
5
+ module Hanami
6
+ class View
7
+ # An exposure defined on a view
8
+ #
9
+ # @api private
10
+ class Exposure
11
+ include Dry::Equalizer(:name, :proc, :object, :options)
12
+
13
+ EXPOSURE_DEPENDENCY_PARAMETER_TYPES = %i[req opt].freeze
14
+ INPUT_PARAMETER_TYPES = %i[key keyreq keyrest].freeze
15
+
16
+ attr_reader :name
17
+ attr_reader :proc
18
+ attr_reader :object
19
+ attr_reader :options
20
+
21
+ def initialize(name, proc = nil, object = nil, **options)
22
+ @name = name
23
+ @proc = prepare_proc(proc, object)
24
+ @object = object
25
+ @options = options
26
+ end
27
+
28
+ def bind(obj)
29
+ self.class.new(name, proc, obj, **options)
30
+ end
31
+
32
+ def dependency_names
33
+ if proc
34
+ proc.parameters.each_with_object([]) { |(type, name), names|
35
+ names << name if EXPOSURE_DEPENDENCY_PARAMETER_TYPES.include?(type)
36
+ }
37
+ else
38
+ []
39
+ end
40
+ end
41
+
42
+ def input_keys
43
+ if proc
44
+ proc.parameters.each_with_object([]) { |(type, name), keys|
45
+ keys << name if INPUT_PARAMETER_TYPES.include?(type)
46
+ }
47
+ else
48
+ []
49
+ end
50
+ end
51
+
52
+ def for_layout?
53
+ options.fetch(:layout) { false }
54
+ end
55
+
56
+ def decorate?
57
+ options.fetch(:decorate) { true }
58
+ end
59
+
60
+ def private?
61
+ options.fetch(:private) { false }
62
+ end
63
+
64
+ def default_value
65
+ options[:default]
66
+ end
67
+
68
+ def call(input, locals = {})
69
+ if proc
70
+ call_proc(input, locals)
71
+ else
72
+ input.fetch(name) { default_value }
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def call_proc(input, locals)
79
+ args, keywords = proc_args(input, locals)
80
+
81
+ if keywords.empty?
82
+ if proc.is_a?(Method)
83
+ proc.(*args)
84
+ else
85
+ object.instance_exec(*args, &proc)
86
+ end
87
+ else
88
+ if proc.is_a?(Method)
89
+ proc.(*args, **keywords)
90
+ else
91
+ object.instance_exec(*args, **keywords, &proc)
92
+ end
93
+ end
94
+ end
95
+
96
+ def proc_args(input, locals)
97
+ dependency_args = proc_dependency_args(locals)
98
+ keywords = proc_input_args(input)
99
+
100
+ if keywords.empty?
101
+ [dependency_args, {}]
102
+ else
103
+ [dependency_args, keywords]
104
+ end
105
+ end
106
+
107
+ def proc_dependency_args(locals)
108
+ dependency_names.map { |name| locals.fetch(name) }
109
+ end
110
+
111
+ def proc_input_args(input)
112
+ input_keys.each_with_object({}) { |key, args|
113
+ args[key] = input[key] if input.key?(key)
114
+ }
115
+ end
116
+
117
+ def prepare_proc(proc, object)
118
+ if proc
119
+ proc
120
+ elsif object.respond_to?(name, _include_private = true)
121
+ object.method(name)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end