From e5049b759d64eeeb302ed290f7f7885e596bfca9 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Tue, 29 Jan 2019 21:55:17 +1100 Subject: [PATCH] Install yard and add initial API docs --- .yardopts | 5 + Gemfile | 6 + lib/dry/view.rb | 428 ++++++++++++++++++--- lib/dry/view/context.rb | 47 +++ lib/dry/view/decorated_attributes.rb | 19 + lib/dry/view/exposure.rb | 3 + lib/dry/view/exposures.rb | 1 + lib/dry/view/part.rb | 121 ++++++ lib/dry/view/part_builder.rb | 18 +- lib/dry/view/path.rb | 1 + lib/dry/view/render_environment.rb | 1 + lib/dry/view/render_environment_missing.rb | 1 + lib/dry/view/rendered.rb | 27 ++ lib/dry/view/renderer.rb | 1 + lib/dry/view/scope.rb | 96 +++++ lib/dry/view/scope_builder.rb | 24 +- lib/dry/view/tilt.rb | 1 + lib/dry/view/version.rb | 1 + 18 files changed, 753 insertions(+), 48 deletions(-) create mode 100644 .yardopts diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..5cfd8d4 --- /dev/null +++ b/.yardopts @@ -0,0 +1,5 @@ +--query '@api.text != "private"' +--markup-provider=redcarpet +--markup=markdown +--plugin junk +lib/**/*.rb diff --git a/Gemfile b/Gemfile index a92cdac..37a38b7 100644 --- a/Gemfile +++ b/Gemfile @@ -24,3 +24,9 @@ group :benchmarks do gem 'actionview' gem 'actionpack' end + +group :docs do + gem 'yard' + gem 'yard-junk' + gem 'redcarpet', platforms: :mri +end diff --git a/lib/dry/view.rb b/lib/dry/view.rb index 49f9eef..f486013 100644 --- a/lib/dry/view.rb +++ b/lib/dry/view.rb @@ -12,42 +12,210 @@ require_relative 'view/rendered' require_relative 'view/renderer' require_relative 'view/scope_builder' +# A collection of next-generation Ruby libraries, helping you to write clear, +# flexible, and more maintainable Ruby code. Each dry-rb gem fulfils a common +# task, and together they make a powerful platform for any kind of Ruby +# application. module Dry + # A standalone, template-based view rendering system that offers everything + # you need to write well-factored view code. + # + # This represents a single view, holding the configuration and exposures + # necessary for rendering its template. + # + # @abstract Subclass this and provide your own configuration and exposures to + # define your own view (along with a custom `#initialize` if you wish to + # inject dependencies into your subclass) + # + # @see https://dry-rb.org/gems/dry-view/ + # + # @api public class View - extend Dry::Core::Cache - + # @api private UndefinedTemplateError = Class.new(StandardError) + # @api private DEFAULT_RENDERER_OPTIONS = {default_encoding: 'utf-8'.freeze}.freeze include Dry::Equalizer(:config, :exposures) + extend Dry::Core::Cache + extend Dry::Configurable + # @!group Configuration + + # @overload config.paths=(paths) + # Set an array of directories that will be searched for all templates + # (templates, partials, and layouts). + # + # These will be converted into Path objects and used for template lookup + # when rendering. + # + # This is a **required setting**. + # + # @param paths [String, Path, Array] the paths + # + # @api public + # @!scope class setting :paths do |paths| Array(paths).map { |path| Path[path] } end - setting :layout, false - setting :layouts_dir, "layouts".freeze + + # @overload config.template=(name) + # Set the name of the template for rendering this view. Template name + # should be relative to the configured `paths`. + # + # This is a **required setting**. + # + # @param name [String] template name + # @api public + # @!scope class setting :template + # @overload config.layout=(name) + # Set the name of the layout to render templates within. Layouts will be + # looked up within the configured `layouts_dir`, within the configured + # `paths`. + # + # A false or nil value will use no layout. Defaults to `nil`. + # + # @param name [String, FalseClass, nil] layout name, or false to indicate no layout + # @api public + # @!scope class + setting :layout, false + + # @overload config.layouts_dir=(dir) + # Set the name of the directory (within the configured `paths`) holding + # the layouts. Defaults to `"layouts"` + # + # @param dir [String] directory name + # @api public + # @!scope class + setting :layouts_dir, "layouts".freeze + + # @overload config.scope=(scope_class) + # Set the scope class to use when rendering the view's template. + # + # Configuring a custom scope class allows you to provide extra behaviour + # (alongside exposures) to the template. + # + # @see https://dry-rb.org/gems/dry-view/scopes/ + # + # @param scope_class [Class] scope class (inheriting from `Dry::View::Scope`) + # @api public + # @!scope class + setting :scope + + # @overload config.default_context=(context) + # Set the default context object to use when rendering. This will be used + # unless another context object is applied at render-time to `View#call` + # + # Defaults to a frozen instance of `Dry::View::Context`. + # + # @see View#call + # + # @param context [Dry::View::Context] context object + # @api public + # @!scope class + setting :default_context, Context.new.freeze + + # @overload config.default_format=(format) + # Set the default format to use when rendering. + # + # Defaults to `:html`. + # + # @param format [Symbol] + # @api public + # @!scope class setting :default_format, :html - setting :renderer_engine_mapping + + # @overload config.scope_namespace=(namespace) + # Set a namespace that will be searched when building scope classes. + # + # @param namespace [Module, Class] + # + # @see Scope + # + # @api public + # @!scope class + setting :part_namespace + + # @overload config.part_builder=(part_builder) + # Set a custom part builder class + # + # @see https://dry-rb.org/gems/dry-view/parts/ + # + # @param part_builder [Class] + # @api public + # @!scope class + setting :part_builder, PartBuilder + + # @overload config.scope_namespace=(namespace) + # Set a namespace that will be searched when building scope classes. + # + # @param namespace [Module, Class] + # + # @see Scope + # + # @api public + # @!scope class + setting :scope_namespace + + # @overload config.scope_builder=(scope_builder) + # Set a custom scope builder class + # + # @see https://dry-rb.org/gems/dry-view/scopes/ + # + # @param scope_builder [Class] + # @api public + # @!scope class + setting :scope_builder, ScopeBuilder + + # @overload config.inflector=(inflector) + # Set an inflector to provide to the part_builder and scope_builder. + # + # Defaults to `Dry::Inflector.new`. + # + # @param inflector + # @api public + # @!scope class + setting :inflector, Dry::Inflector.new + + # @overload config.renderer_options=(options) + # A hash of options to pass to the template engine. Template engines are + # provided by Tilt; see Tilt's documentation for what options your + # template engine may support. + # + # Defaults to `{default_encoding: "utf-8"}`. Any options passed will be + # merged onto the defaults. + # + # @see https://github.com/rtomayko/tilt + # + # @param options [Hash] renderer options + # @api public + # @!scope class setting :renderer_options, DEFAULT_RENDERER_OPTIONS do |options| DEFAULT_RENDERER_OPTIONS.merge(options.to_h).freeze end - setting :default_context, Context.new.freeze + # @overload config.renderer_engine_mapping=(mapping) + # A hash specifying the (Tilt-compatible) template engine class to use + # for a given format. Template engine detection is automatic based on + # format; use this setting only if you want to force a non-preferred + # engine. + # + # @example + # config.renderer_engine_mapping = {erb: Tilt::ErubiTemplate} + # + # @see https://github.com/rtomayko/tilt + # + # @param mapping [Hash] engine mapping + # @api public + # @!scope class + setting :renderer_engine_mapping - setting :scope - - setting :inflector, Dry::Inflector.new - - setting :part_builder, PartBuilder - setting :part_namespace - - setting :scope_builder, ScopeBuilder - setting :scope_namespace + # @!endgroup # @api private def self.inherited(klass) @@ -57,38 +225,111 @@ module Dry end end - # @api private - def self.layout_path - File.join(config.layouts_dir, config.layout) - end + # @!group Exposures - # @api public - def self.render_env(format: config.default_format, context: config.default_context) - RenderEnvironment.prepare(renderer(format), config, context) - end - - # @api public - def self.template_env(**args) - render_env(**args).chdir(config.template) - end - - # @api public - def self.layout_env(**args) - render_env(**args).chdir(layout_path) - end - - # @api private - def self.renderer(format) - fetch_or_store(:renderer, config, format) { - Renderer.new( - config.paths, - format: format, - engine_mapping: config.renderer_engine_mapping, - **config.renderer_options, - ) - } - end + # @!macro [new] exposure_options + # @param options [Hash] the exposure's options + # @option options [Boolean] :layout expose this value to the layout (defaults to false) + # @option options [Boolean] :decorate decorate this value in a matching Part (defaults to true) + # @option options [Symbol, Class] :as an alternative name or class to use when finding a matching Part + # @overload expose(name, **options, &block) + # Define a value to be passed to the template. The return value of the + # block will be decorated by a matching Part and passed to the template. + # + # The block will be evaluated with the view instance as its `self`. The + # block's parameters will determine what it is given: + # + # - To receive other exposure values, provide positional parameters + # matching the exposure names. These exposures will already by decorated + # by their Parts. + # - To receive the view's input arguments (whatever is passed to + # `View#call`), provide matching keyword parameters. You can provide + # default values for these parameters to make the corresponding input + # keys optional + # - To receive the Context object, provide a `context:` keyword parameter + # - To receive the view's input arguments in their entirety, provide a + # keywords splat parameter (i.e. `**input`) + # + # @example Accessing input arguments + # expose :article do |slug:| + # article_repo.find_by_slug(slug) + # end + # + # @example Accessing other exposures + # expose :articles do + # article_repo.listing + # end + # + # expose :featured_articles do |articles| + # articles.select(&:featured?) + # end + # + # @param name [Symbol] name for the exposure + # @macro exposure_options + # + # @overload expose(name, **options) + # Define a value to be passed to the template, provided by an instance + # method matching the name. The method's return value will be decorated by + # a matching Part and passed to the template. + # + # The method's parameters will determine what it is given: + # + # - To receive other exposure values, provide positional parameters + # matching the exposure names. These exposures will already by decorated + # by their Parts. + # - To receive the view's input arguments (whatever is passed to + # `View#call`), provide matching keyword parameters. You can provide + # default values for these parameters to make the corresponding input + # keys optional + # - To receive the Context object, provide a `context:` keyword parameter + # - To receive the view's input arguments in their entirey, provide a + # keywords splat parameter (i.e. `**input`) + # + # @example Accessing input arguments + # expose :article + # + # def article(slug:) + # article_repo.find_by_slug(slug) + # end + # + # @example Accessing other exposures + # expose :articles + # expose :featured_articles + # + # def articles + # article_repo.listing + # end + # + # def featured_articles + # articles.select(&:featured?) + # end + # + # @param name [Symbol] name for the exposure + # @macro exposure_options + # + # @overload expose(name, **options) + # Define a single value to pass through from the input data (when there is + # no instance method matching the `name`). This value will be decorated by + # a matching Part and passed to the template. + # + # @param name [Symbol] name for the exposure + # @macro exposure_options + # @option options [Boolean] :default a default value to provide if there is no matching input data + # + # @overload expose(*names, **options) + # Define multiple values to pass through from the input data (when there + # is no instance methods matching their names). These values will be + # decorated by matching Parts and passed through to the template. + # + # The provided options will be applied to all the exposures. + # + # @param names [Symbol] names for the exposures + # @macro exposure_options + # @option options [Boolean] :default a default value to provide if there is no matching input data + # + # @see https://dry-rb.org/gems/dry-view/exposures/ + # # @api public def self.expose(*names, **options, &block) if names.length == 1 @@ -105,23 +346,115 @@ module Dry expose(*names, **options, private: true, &block) end + # Returns the defined exposures. These are unbound, since bound exposures + # are only created when initializing a View instance. + # + # @return [Exposures] # @api private def self.exposures @exposures ||= Exposures.new end + # @!endgroup + + # @!group Render environment + + # Returns a render environment for the view and the given options. This + # environment isn't chdir'ed into any particular directory. + # + # @param format [Symbol] template format to use (defaults to the `default_format` setting) + # @param context [Context] context object to use (defaults to the `default_context` setting) + # + # @see View.template_env render environment for the view's template + # @see View.layout_env render environment for the view's layout + # + # @return [RenderEnvironment] + # @api public + def self.render_env(format: config.default_format, context: config.default_context) + RenderEnvironment.prepare(renderer(format), config, context) + end + + # @overload template_env(format: config.default_format, context: config.default_context) + # Returns a render environment for the view and the given options, + # chdir'ed into the view's template directory. This is the environment + # used when rendering the template, and is useful to to fetch + # independently when unit testing Parts and Scopes. + # + # @param format [Symbol] template format to use (defaults to the `default_format` setting) + # @param context [Context] context object to use (defaults to the `default_context` setting) + # + # @return [RenderEnvironment] + # @api public + def self.template_env(**args) + render_env(**args).chdir(config.template) + end + + # @overload layout_env(format: config.default_format, context: config.default_context) + # Returns a render environment for the view and the given options, + # chdir'ed into the view's layout directory. This is the environment used + # when rendering the view's layout. + # + # @param format [Symbol] template format to use (defaults to the `default_format` setting) + # @param context [Context] context object to use (defaults to the `default_context` setting) + # + # @return [RenderEnvironment] @api public + def self.layout_env(**args) + render_env(**args).chdir(layout_path) + end + + # Returns renderer for the view and provided format + # + # @api private + def self.renderer(format) + fetch_or_store(:renderer, config, format) { + Renderer.new( + config.paths, + format: format, + engine_mapping: config.renderer_engine_mapping, + **config.renderer_options, + ) + } + end + + # @api private + def self.layout_path + File.join(config.layouts_dir, config.layout) + end + + # @!endgroup + + # The view's bound exposures + # + # @return [Exposures] + # @api private attr_reader :exposures + # Returns an instance of the view. This binds the defined exposures to the + # view instance. + # + # Subclasses can define their own `#initialize` to accept injected + # dependencies, but must call `super()` to ensure the standard view + # initialization can proceed. + # # @api public def initialize @exposures = self.class.exposures.bind(self) end - # @api public + # The view's configuration + # + # @api private def config self.class.config end + # Render the view + # + # @param format [Symbol] template format to use + # @param context [Context] context object to use + # @param input input data for preparing exposure values + # + # @return [Rendered] rendered view object # @api public def call(format: config.default_format, context: config.default_context, **input) raise UndefinedTemplateError, "no +template+ configured" unless config.template @@ -142,6 +475,7 @@ module Dry private + # @api private def locals(render_env, input) exposures.(context: render_env.context, **input) do |value, exposure| if exposure.decorate? && value @@ -152,12 +486,14 @@ module Dry end end + # @api private def layout_locals(locals) locals.each_with_object({}) do |(key, value), layout_locals| layout_locals[key] = value if exposures[key].for_layout? end end + # @api private def layout? !!config.layout end diff --git a/lib/dry/view/context.rb b/lib/dry/view/context.rb index 9b46499..0cf101b 100644 --- a/lib/dry/view/context.rb +++ b/lib/dry/view/context.rb @@ -3,23 +3,70 @@ require_relative "decorated_attributes" module Dry class View + # Provides a baseline environment across all the templates, parts and scopes + # in a given rendering. + # + # @abstract Subclass this and add your own methods (along with a custom + # `#initialize` if you wish to inject dependencies) + # + # @api public class Context include Dry::Equalizer(:_options) include DecoratedAttributes attr_reader :_render_env, :_options + # Returns a new instance of Context + # + # In subclasses, you should include an `**options` parameter and pass _all + # arguments_ to `super`. This allows Context to make copies of itself + # while preserving your dependencies. + # + # @example + # class MyContext < Dry::View::Context + # # Injected dependency + # attr_reader :assets + # + # def initialize(assets:, **options) + # @assets = assets + # super + # end + # end + # + # @api public def initialize(render_env: nil, **options) @_render_env = render_env @_options = options end + # @api private def for_render_env(render_env) return self if render_env == self._render_env self.class.new(**_options.merge(render_env: render_env)) end + # Returns a copy of the Context with new options merged in. + # + # This may be useful to supply values for dependencies that are _optional_ + # when initializing your custom Context subclass. + # + # @example + # class MyContext < Dry::View::Context + # # Injected dependencies (request is optional) + # attr_reader :assets, :request + # + # def initialize(assets:, request: nil, **options) + # @assets = assets + # @request = reuqest + # super + # end + # end + # + # my_context = MyContext.new(assets: assets) + # my_context_with_request = my_context.with(request: request) + # + # @api public def with(**new_options) self.class.new( render_env: _render_env, diff --git a/lib/dry/view/decorated_attributes.rb b/lib/dry/view/decorated_attributes.rb index b6e631b..73718ed 100644 --- a/lib/dry/view/decorated_attributes.rb +++ b/lib/dry/view/decorated_attributes.rb @@ -2,14 +2,32 @@ require "set" module Dry class View + # Decorates attributes in Parts. module DecoratedAttributes + # @api private def self.included(klass) klass.extend ClassInterface end + # Decorated attributes class-level interface. module ClassInterface + # @api private MODULE_NAME = :DecoratedAttributes + # Decorates the provided attributes, wrapping them in Parts using the + # current render environment. + # + # @example + # class Article < Dry::View::Part + # decorate :feature_image + # decorate :author as: :person + # end + # + # @param names [Array] the attribute names + # @param options [Hash] the options to pass to the Part Builder + # @option options [Symbol, Class] :as an alternative name or class to use when finding a matching Part + # + # @api public def decorate(*names, **options) decorated_attributes.decorate(*names, **options) end @@ -27,6 +45,7 @@ module Dry end end + # @api private class Attributes < Module def initialize(*) @names = Set.new diff --git a/lib/dry/view/exposure.rb b/lib/dry/view/exposure.rb index bb243a7..6c87452 100644 --- a/lib/dry/view/exposure.rb +++ b/lib/dry/view/exposure.rb @@ -2,6 +2,9 @@ require 'dry-equalizer' module Dry class View + # An exposure defined on a view + # + # @api private class Exposure include Dry::Equalizer(:name, :proc, :object, :options) diff --git a/lib/dry/view/exposures.rb b/lib/dry/view/exposures.rb index 5330c4f..5fae83a 100644 --- a/lib/dry/view/exposures.rb +++ b/lib/dry/view/exposures.rb @@ -4,6 +4,7 @@ require "dry/view/exposure" module Dry class View + # @api private class Exposures include Dry::Equalizer(:exposures) include TSort diff --git a/lib/dry/view/part.rb b/lib/dry/view/part.rb index e55a637..6d4cef6 100644 --- a/lib/dry/view/part.rb +++ b/lib/dry/view/part.rb @@ -4,7 +4,17 @@ require_relative "render_environment_missing" module Dry class View + # Decorates an exposure value and provides a place to encapsulate + # view-specific behavior alongside your application's domain objects. + # + # @abstract Subclass this and provide your own methods adding view-specific + # behavior. You should not override `#initialize`. + # + # @see https://dry-rb.org/gems/dry-view/parts/ + # + # @api public class Part + # @api private CONVENIENCE_METHODS = %i[ format context @@ -16,42 +26,146 @@ module Dry include Dry::Equalizer(:_name, :_value, :_render_env) include DecoratedAttributes + # The part's name. This comes from the exposure supplying the value. + # + # @return [Symbol] name + # + # @api public attr_reader :_name + # The decorated value. This is the value returned from the exposure. + # + # @overload _value + # Returns the value. + # @overload value + # A convenience alias for `_value`. Is available unless the value itself + # responds to `#value`. + # + # @return [Object] value + # + # @api public attr_reader :_value + # The current render environment + # + # @return [RenderEnvironment] render environment + # + # @api private attr_reader :_render_env + # Determins a part name (when initialized without one). Intended for use + # only while unit testing Parts. + # + # @api private def self.part_name(inflector) name ? inflector.underscore(inflector.demodulize(name)) : "part" end + # Returns a new Part instance + # + # @param name [Symbol] part name + # @param value [Object] the value to decorate + # @param render_env [RenderEnvironment] render environment + # + # @api public def initialize(render_env: RenderEnvironmentMissing.new, name: self.class.part_name(render_env.inflector), value:) @_name = name @_value = value @_render_env = render_env end + # The template format for the current render environment. + # + # @overload _format + # Returns the format. + # @overload format + # A convenience alias for `#_format.` Is available unless the value + # itself responds to `#format`. + # + # @return [Symbol] format + # + # @api public def _format _render_env.format end + # The context object for the current render environment + # + # @overload _context + # Returns the context. + # @overload context + # A convenience alias for `#_context`. Is available unless the value + # itself responds to `#context`. + # + # @return [Context] context + # + # @api public def _context _render_env.context end + # Renders a new partial with the part included in its locals. + # + # @overload _render(partial_name, as: _name, **locals, &block) + # Renders the partial. + # @overload render(partial_name, as: _name, **locals, &block) + # A convenience alias for `#_render`. Is available unless the value + # itself responds to `#render`. + # + # @param partial_name [Symbol, String] partial name + # @param as [Symbol] the name for the Part to assume in the partial's locals. Default's to the Part's `_name`. + # @param locals [Hash] other locals to provide the partial + # + # @return [String] rendered partial + # + # @api public def _render(partial_name, as: _name, **locals, &block) _render_env.partial(partial_name, _render_env.scope({as => self}.merge(locals)), &block) end + # Builds a new scope with the part included in its locals. + # + # @overload _scope(scope_name = nil, **locals) + # Builds the scope. + # @overload scope(scope_name = nil, **locals) + # A convenience alias for `#_scope`. Is available unless the value + # itself responds to `#scope`. + # + # @param scope_name [Symbol, nil] scope name, used by the scope builder to determine the scope class + # @param locals [Hash] other locals to provide the partial + # + # @return [Dry::View::Scope] scope + # + # @api public def _scope(scope_name = nil, **locals) _render_env.scope(scope_name, {_name => self}.merge(locals)) end + # Returns a string representation of the value + # + # @return [String] + # + # @api public def to_s _value.to_s end + # Builds a new a part with the given parameters + # + # This is helpful for manually constructing a new part object that + # maintains the current render environment. + # + # However, using `.decorate` is preferred for declaring attributes that + # should also be decorated as parts. + # + # @see DecoratedAttributes::ClassInterface#decorate + # + # @param klass [Class] part class to use (defaults to the part's class) + # @param name [Symbol] part name (defaults to the part's name) + # @param value [Object] value to decorate (defaults to the part's value) + # @param options[Hash] other options to provide when initializing the new part + # + # @api public def new(klass = (self.class), name: (_name), value: (_value), **options) klass.new( name: name, @@ -61,12 +175,19 @@ module Dry ) end + # Returns a string representation of the part + # + # @return [String] + # + # @api public def inspect %(#<#{self.class.name} name=#{_name.inspect} value=#{_value.inspect}>) end private + # Handles missing methods. If the `_value` responds to the method, then + # the method will be sent to the value. def method_missing(name, *args, &block) if _value.respond_to?(name) _value.public_send(name, *args, &block) diff --git a/lib/dry/view/part_builder.rb b/lib/dry/view/part_builder.rb index 39fcc2d..86efe88 100644 --- a/lib/dry/view/part_builder.rb +++ b/lib/dry/view/part_builder.rb @@ -4,6 +4,9 @@ require_relative 'part' module Dry class View + # Decorates exposure values with matching parts + # + # @api private class PartBuilder extend Dry::Core::Cache include Dry::Equalizer(:namespace) @@ -11,17 +14,30 @@ module Dry attr_reader :namespace attr_reader :render_env + # Returns a new instance of PartBuilder + # + # @api private def initialize(namespace: nil, render_env: nil) @namespace = namespace @render_env = render_env end + # @api private def for_render_env(render_env) return self if render_env == self.render_env self.class.new(namespace: namespace, render_env: render_env) end + # Decorates an exposure value + # + # @param name [Symbol] exposure name + # @param value [Object] exposure value + # @param options [Hash] exposure options + # + # @return [Dry::View::Part] decorated value + # + # @api private def call(name, value, **options) builder = value.respond_to?(:to_ary) ? :build_collection_part : :build_part @@ -93,7 +109,7 @@ module Dry name = inflector.camelize(name.to_s) - # Give autoloaders a change to act + # Give autoloaders a chance to act begin klass = namespace.const_get(name) rescue NameError diff --git a/lib/dry/view/path.rb b/lib/dry/view/path.rb index a244fb8..77c68b0 100644 --- a/lib/dry/view/path.rb +++ b/lib/dry/view/path.rb @@ -3,6 +3,7 @@ require "dry/core/cache" module Dry class View + # @api private class Path extend Dry::Core::Cache include Dry::Equalizer(:dir, :root) diff --git a/lib/dry/view/render_environment.rb b/lib/dry/view/render_environment.rb index 9d2ad69..615ed6c 100644 --- a/lib/dry/view/render_environment.rb +++ b/lib/dry/view/render_environment.rb @@ -2,6 +2,7 @@ require "dry/equalizer" module Dry class View + # @api private class RenderEnvironment def self.prepare(renderer, config, context) new( diff --git a/lib/dry/view/render_environment_missing.rb b/lib/dry/view/render_environment_missing.rb index 3bce1f8..c4691ad 100644 --- a/lib/dry/view/render_environment_missing.rb +++ b/lib/dry/view/render_environment_missing.rb @@ -2,6 +2,7 @@ require "dry/inflector" module Dry class View + # @api private class RenderEnvironmentMissing class MissingEnvironmentError < StandardError def message diff --git a/lib/dry/view/rendered.rb b/lib/dry/view/rendered.rb index 5a74c5f..36ea29b 100644 --- a/lib/dry/view/rendered.rb +++ b/lib/dry/view/rendered.rb @@ -2,21 +2,48 @@ require 'dry/equalizer' module Dry class View + # Output of a View rendering + # + # @api public class Rendered include Dry::Equalizer(:output, :locals) + # Returns the rendered view + # + # @return [String] + # + # @api public attr_reader :output + + # Returns the hash of locals used to render the view + # + # @return [Hash[] locals hash + # + # @api public attr_reader :locals + # @api private def initialize(output:, locals:) @output = output @locals = locals end + # Returns the local corresponding to the key + # + # @param name [Symbol] local key + # + # @return [Dry::View::Part] + # + # @api public def [](name) locals[name] end + # Returns the rendered view + # + # @return [String] + # + # @api public def to_s output end diff --git a/lib/dry/view/renderer.rb b/lib/dry/view/renderer.rb index 80e04be..c025bf3 100644 --- a/lib/dry/view/renderer.rb +++ b/lib/dry/view/renderer.rb @@ -4,6 +4,7 @@ require_relative 'tilt' module Dry class View + # @api private class Renderer PARTIAL_PREFIX = "_".freeze PATH_DELIMITER = "/".freeze diff --git a/lib/dry/view/scope.rb b/lib/dry/view/scope.rb index 2614579..fd17682 100644 --- a/lib/dry/view/scope.rb +++ b/lib/dry/view/scope.rb @@ -4,21 +4,81 @@ require_relative "render_environment_missing" module Dry class View + # Evaluation context for templates (including layouts and partials) and + # provides a place to encapsulate view-specific behaviour alongside a + # template and its locals. + # + # @abstract Subclass this and provide your own methods adding view-specific + # behavior. You should not override `#initialize` + # + # @see https://dry-rb.org/gems/dry-view/templates/ + # @see https://dry-rb.org/gems/dry-view/scopes/ + # + # @api public class Scope + # @api private CONVENIENCE_METHODS = %i[format context locals].freeze include Dry::Equalizer(:_name, :_locals, :_render_env) + # The scope's name + # + # @return [Symbol] + # + # @api public attr_reader :_name + + # The scope's locals + # + # @overload _locals + # Returns the locals + # @overload locals + # A convenience alias for `#_format.` Is available unless there is a + # local named `locals` + # + # @return [Hash[] + # + # @api public attr_reader :_locals + + # The current render environment + # + # @return [RenderEnvironment] render environment + # + # @api private attr_reader :_render_env + # Returns a new Scope instance + # + # @param name [Symbol, nil] scope name + # @param locals [Hash] template locals + # @param render_env [RenderEnvironment] render environment + # + # @return [Scope] + # + # @api public def initialize(name: nil, locals: Dry::Core::Constants::EMPTY_HASH, render_env: RenderEnvironmentMissing.new) @_name = name @_locals = locals @_render_env = render_env end + # @overload render(partial_name, **locals, &block) + # Renders a partial using the scope + # + # @param partial_name [Symbol, String] partial name + # @param locals [Hash] partial locals + # @yieldreturn [String] string content to include where the partial calls `yield` + # + # @overload render(**locals, &block) + # Renders a partial (named after the scope's own name) using the scope + # + # @param locals[Hash] partial locals + # @yieldreturn [String] string content to include where the partial calls `yield` + # + # @return [String] the rendered partial output + # + # @api public def render(partial_name = nil, **locals, &block) partial_name ||= _name raise ArgumentError, "+partial_name+ must be provided for unnamed scopes" unless partial_name @@ -27,20 +87,56 @@ module Dry _render_env.partial(partial_name, _render_scope(locals), &block) end + # Build a new scope using a scope class matching the provided name + # + # @param name [Symbol, Class] scope name (or class) + # @param locals [Hash] scope locals + # + # @return [Scope] + # + # @api public def scope(name = nil, **locals) _render_env.scope(name, locals) end + # The template format for the current render environment. + # + # @overload _format + # Returns the format. + # @overload format + # A convenience alias for `#_format.` Is available unless there is a + # local named `format` + # + # @return [Symbol] format + # + # @api public def _format _render_env.format end + # The context object for the current render environment + # + # @overload _context + # Returns the context. + # @overload context + # A convenience alias for `#_context`. Is available unless there is a + # local named `context`. + # + # @return [Context] context + # + # @api public def _context _render_env.context end private + # Handles missing methods, according to the following rules: + # + # 1. If there is a local with a name matching the method, it returns the + # local. + # 2. If the `context` responds to the method, then it will be sent the + # method and all its arguments. def method_missing(name, *args, &block) if _locals.key?(name) _locals[name] diff --git a/lib/dry/view/scope_builder.rb b/lib/dry/view/scope_builder.rb index 9d28316..0a18d77 100644 --- a/lib/dry/view/scope_builder.rb +++ b/lib/dry/view/scope_builder.rb @@ -4,24 +4,46 @@ require_relative 'scope' module Dry class View + # Builds scope objects via matching classes + # + # @api private class ScopeBuilder extend Dry::Core::Cache include Dry::Equalizer(:namespace) + # The view's configured `scope_namespace` + # + # @api private attr_reader :namespace + + # @return [RenderEnvironment] + # + # @api private attr_reader :render_env + # Returns a new instance of ScopeBuilder + # + # @api private def initialize(namespace: nil, render_env: nil) @namespace = namespace @render_env = render_env end + # @api private def for_render_env(render_env) return self if render_env == self.render_env self.class.new(namespace: namespace, render_env: render_env) end + # Returns a new scope using a class matching the name + # + # @param name [Symbol, Class] scope name + # @param locals [Hash] locals hash + # + # @return [Dry::View::Scope] + # + # @api private def call(name = nil, locals) scope_class(name).new( name: name, @@ -49,7 +71,7 @@ module Dry def resolve_scope_class(name:) name = inflector.camelize(name.to_s) - # Give autoloaders a change to act + # Give autoloaders a chance to act begin klass = namespace.const_get(name) rescue NameError diff --git a/lib/dry/view/tilt.rb b/lib/dry/view/tilt.rb index fd0302f..93257df 100644 --- a/lib/dry/view/tilt.rb +++ b/lib/dry/view/tilt.rb @@ -3,6 +3,7 @@ require "tilt" module Dry class View + # @api private module Tilt extend Dry::Core::Cache diff --git a/lib/dry/view/version.rb b/lib/dry/view/version.rb index 03f002d..cab9f9c 100644 --- a/lib/dry/view/version.rb +++ b/lib/dry/view/version.rb @@ -1,5 +1,6 @@ module Dry class View + # Current release version VERSION = '0.5.4'.freeze end end