Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ eval_gemfile "Gemfile.devtools"

gemspec

gem "dry-configurable", github: "dry-rb/dry-configurable", branch: "main"

group :tools do
# Remove hotch until https://github.com/ko1/allocation_tracer/issues/19 is fixed and it can be
# installed on macOS again.
Expand Down Expand Up @@ -34,6 +36,8 @@ group :benchmarks do
gem "actionpack"
gem "actionview"
gem "benchmark-ips"
gem "stackprof", platform: :mri
gem "memory_profiler"
Comment on lines +39 to +40
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these not strictly needed for this PR?

end

group :docs do
Expand Down
31 changes: 18 additions & 13 deletions lib/hanami/view.rb
Original file line number Diff line number Diff line change
Expand Up @@ -540,12 +540,6 @@ def self.scope(scope_class = nil, &block)

# @!endgroup

# @api private
# @since 2.1.0
def self.layout_path(layout)
File.join(*[config.layouts_dir, layout].compact)
end

# @api private
# @since 2.1.0
def self.cache
Expand All @@ -563,6 +557,7 @@ def initialize
self.class.config.finalize!
ensure_config

@config_data = config.to_data
@exposures = self.class.exposures.bind(self)
end

Expand Down Expand Up @@ -595,16 +590,20 @@ def exposures # rubocop:disable Style/TrivialAccessors
#
# @api public
# @since 2.1.0
def call(format: config.default_format, context: config.default_context, layout: config.layout, **input)
def call(format: config_data.default_format,
context: config_data.default_context,
layout: config_data.layout,
**input)
rendering = self.rendering(format: format, context: context)
scope_class = config_data.scope

locals = locals(rendering, input)
output = rendering.template(config.template, rendering.scope(config.scope, locals))
output = rendering.template(config_data.template, rendering.scope(scope_class, locals))

if layout
output = rendering.template(
self.class.layout_path(layout),
rendering.scope(config.scope, layout_locals(locals))
File.join(*[config_data.layouts_dir, layout].compact),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we leave the layout_path method back as it was? I think it makes this code here (which is already doing a lot!) easier to follow. One fewer piece of detailed implementation inlined into the method.

rendering.scope(scope_class, layout_locals(locals))
) { output }
end

Expand All @@ -613,20 +612,26 @@ def call(format: config.default_format, context: config.default_context, layout:

# @api private
# @since 2.1.0
def rendering(format: config.default_format, context: config.default_context)
Rendering.new(config: config, format: format, context: context)
def rendering(format: config_data.default_format, context: config_data.default_context)
Rendering.new(config_data:, format:, context:)
end

private

# Frozen Data snapshot of the view's resolved configuration values.
# Used for fast hot-path reads during rendering.
#
# @since 2.3.0
Comment on lines +623 to +624
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#
# @since 2.3.0

Another since we can drop. This is not just "@api private", it's fully private :)

attr_reader :config_data

def ensure_config
raise UndefinedConfigError, :paths unless Array(config.paths).any?
raise UndefinedConfigError, :template unless config.template
end

def locals(rendering, input)
exposures.(context: rendering.context, **input) do |value, exposure|
if exposure.decorate?(default: config.decorate_exposures) && value
if exposure.decorate?(default: config_data.decorate_exposures) && value
rendering.part(exposure.name, value, as: exposure.options[:as])
else
value
Expand Down
8 changes: 4 additions & 4 deletions lib/hanami/view/part_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ def part_class(name:, as:, rendering:)
if name.is_a?(Class)
name
else
View.cache.fetch_or_store(:part_class, name, rendering.config) do
View.cache.fetch_or_store(:part_class, name, rendering.cache_key) do
resolve_part_class(name: name, rendering: rendering)
end
end
end

def resolve_part_class(name:, rendering:)
namespace = rendering.config.part_namespace
return rendering.config.part_class unless namespace
namespace = rendering.part_namespace
return rendering.part_class unless namespace

name = rendering.inflector.camelize(name.to_s)

Expand All @@ -91,7 +91,7 @@ def resolve_part_class(name:, rendering:)
if klass && klass < Part
klass
else
rendering.config.part_class
rendering.part_class
end
end
end
Expand Down
20 changes: 9 additions & 11 deletions lib/hanami/view/renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,8 @@ class Renderer

# @api private
# @since 2.1.0
attr_reader :config, :prefixes

# @api private
# @since 2.1.0
def initialize(config)
@config = config
def initialize(config_data)
@config_data = config_data
@prefixes = [CURRENT_PATH_PREFIX]
end

Expand All @@ -37,7 +33,7 @@ def template(name, format, scope, &block)

template_path = lookup(name, format)

raise TemplateNotFoundError.new(name, format, config.paths) unless template_path
raise TemplateNotFoundError.new(name, format, config_data.paths) unless template_path

new_prefix = File.dirname(name)
@prefixes << new_prefix unless @prefixes.include?(new_prefix)
Expand All @@ -55,10 +51,12 @@ def partial(name, format, scope, &block)

private

attr_reader :config_data, :prefixes
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note on why these were public before: I've never really cared about leaving public attr readers on objects, even if they're not formally "public API". It's Ruby, so if anyone really wanted to get to some object, there's always a way they can do it, so I never really tried to fight it.

This is not to say I'm opposed to this change, but I thought I'd just share some of the rationale I held when putting these things together in the first place :)

Happy for this change to go in as part of this PR, anyway :)


def lookup(name, format)
View.cache.fetch_or_store(:lookup, name, format, config, prefixes) {
View.cache.fetch_or_store(:lookup, name, format, config_data.object_id, prefixes) {
catch :found do
config.paths.reduce(nil) do |_, path|
config_data.paths.reduce(nil) do |_, path|
prefixes.reduce(nil) do |_, prefix|
result = path.lookup(prefix, name, format)
throw :found, result if result
Expand All @@ -79,8 +77,8 @@ def render(path, scope, &block)
end

def tilt(path)
View.cache.fetch_or_store(:tilt, path, config) {
Hanami::View::Tilt[path, config.renderer_engine_mapping, config.renderer_options]
View.cache.fetch_or_store(:tilt, path, config_data.object_id) {
Hanami::View::Tilt[path, config_data.renderer_engine_mapping, config_data.renderer_options]
}
end
end
Expand Down
35 changes: 27 additions & 8 deletions lib/hanami/view/rendering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,43 @@ class View
class Rendering
# @api private
# @since 2.1.0
attr_reader :config, :format
attr_reader :format

# @api private
# @since 2.1.0
attr_reader :inflector, :part_builder, :scope_builder

# @api private
# @since 2.3.0
attr_reader :part_class, :part_namespace, :scope_class, :scope_namespace

# Stable identity for the underlying config snapshot, suitable as a cache-key component.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Stable identity for the underlying config snapshot, suitable as a cache-key component.
# Stable identity for the underlying config snapshot

Since this is already called cache_key, we probably don't need to repeat ourselves in the description :)

#
# @api private
# @since 2.3.0
Comment on lines +22 to +23
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's stop putting @since tags on new @api private methods. These are for us to use internally only, and putting a @since IMO implies some level of stability to external readers of the code that I don't want to promise. (It's private API, after all!)

I need to make a post about this, but in the meantime I'm trying to stop it proliferating, and removing the tags whenever I edit a file.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Also, for any "since" that is legitimately needed here — now for "@api public" methods only! — it should be @since 3.0.0)

... though I think most of your changes are to private API, AFAICT

attr_reader :cache_key

# @api private
# @since 2.1.0
attr_reader :context, :renderer
attr_reader :context

# @api private
# @since 2.1.0
def initialize(config:, format:, context:)
@config = config
def initialize(config_data:, format:, context:)
@format = format

@inflector = config.inflector
@part_builder = config.part_builder
@scope_builder = config.scope_builder
@inflector = config_data.inflector
@part_builder = config_data.part_builder
@scope_builder = config_data.scope_builder

@part_class = config_data.part_class
@part_namespace = config_data.part_namespace
@scope_class = config_data.scope_class
@scope_namespace = config_data.scope_namespace
@cache_key = config_data.object_id

@context = context.dup_for_rendering(self)
@renderer = Renderer.new(config)
@renderer = Renderer.new(config_data)
end

# @api private
Expand All @@ -54,6 +69,10 @@ def part(name, value, as: nil)
def scope(name = nil, locals) # rubocop:disable Style/OptionalArguments
scope_builder.(name, locals: locals, rendering: self)
end

private

attr_reader :renderer
Comment on lines +72 to +75
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, this is probably why I don't like private attr_readers. As a maintainer looking at this class, I see a bunch at the top of the file, and then we have this random one hanging out all the way at the bottom. It's disjointed and it's hard to get a good overview of the class this way.

Maybe we could just leave them at the top, since attr privacy is not really the purpose of this PR?

end
end
end
8 changes: 4 additions & 4 deletions lib/hanami/view/scope_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ def call(name = nil, locals:, rendering:)

def scope_class(name = nil, rendering:)
if name.nil?
rendering.config.scope_class
rendering.scope_class
elsif name.is_a?(Class)
name
else
View.cache.fetch_or_store(name, rendering.config) do
View.cache.fetch_or_store(name, rendering.cache_key) do
resolve_scope_class(name: name, rendering: rendering)
end
end
Expand All @@ -40,7 +40,7 @@ def scope_class(name = nil, rendering:)
def resolve_scope_class(name:, rendering:)
name = rendering.inflector.camelize(name.to_s)

namespace = rendering.config.scope_namespace
namespace = rendering.scope_namespace

# Give autoloaders a chance to act
begin
Expand All @@ -55,7 +55,7 @@ def resolve_scope_class(name:, rendering:)
if klass && klass < Scope
klass
else
rendering.config.scope_class
rendering.scope_class
end
end
end
Expand Down
3 changes: 2 additions & 1 deletion spec/integration/part/decorated_attributes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def initialize(author:, body:)

let(:rendering) {
Hanami::View::Rendering.new(
config: view.config,
config_data: view.config.to_data,
format: :html,
context: Hanami::View::Context.new
)
Expand All @@ -71,6 +71,7 @@ def initialize(author:, body:)

Class.new(Hanami::View) {
config.part_builder = part_builder if part_builder
finalize!
}
}

Expand Down
4 changes: 3 additions & 1 deletion spec/unit/renderer_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# frozen_string_literal: true

RSpec.describe Hanami::View::Renderer do
subject(:renderer) { Hanami::View::Renderer.new(view_class.config) }
subject(:renderer) { Hanami::View::Renderer.new(config_data) }

let(:config_data) { view_class.config.to_data }

let(:view_class) {
Class.new(Hanami::View) {
Expand Down