class Pakyow::Reflection::Mirror

Reflects state from an application's view templates.

@api private

Constants

IGNORED_ATTRIBUTES

Attributes

actions[R]
endpoints[R]
scopes[R]

Public Class Methods

new(app) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 23
def initialize(app)
  @app, @scopes, @endpoints, @actions = app, [], [], []

  view_paths.each do |view_path|
    discover_view_scopes(view_path: view_path)
  end

  view_paths.each do |view_path|
    discover_view_path_associations(view_path: view_path)
  end
end

Public Instance Methods

scope(name) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 35
def scope(name)
  @scopes.find { |scope|
    scope.named?(name)
  }
end

Private Instance Methods

action_for_form(view, path) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 320
def action_for_form(view, path)
  plural_binding_name = Support.inflector.pluralize(view.binding_name)
  if view.labeled?(:endpoint)
    endpoint = view.label(:endpoint).to_s
    if endpoint.end_with?("#{plural_binding_name}_create")
      :create
    elsif endpoint.end_with?("#{plural_binding_name}_update")
      :update
    elsif endpoint.end_with?("#{plural_binding_name}_delete")
      :delete
    else
      nil
    end
  elsif path.include?(plural_binding_name) && path.include?("edit")
    :update
  else
    :create
  end
end
discover_attributes(view, fields: true) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 241
def discover_attributes(view, fields: true)
  view.binding_props.reject { |binding_prop_node|
    binding_prop_node.significant?(:multipart_binding) && binding_prop_node.label(:binding) != view.binding_name
  }.select { |binding_prop_node|
    !fields || Presenter::Views::Form::FIELD_TAGS.include?(binding_prop_node.tagname)
  }.each_with_object([]) do |binding_prop_node, attributes|
    binding_prop_view = Presenter::View.from_object(binding_prop_node)
    binding_prop_name = if binding_prop_node.significant?(:multipart_binding)
      binding_prop_node.label(:binding_prop)
    else
      binding_prop_node.label(:binding)
    end

    unless IGNORED_ATTRIBUTES.include?(binding_prop_name)
      attribute = Attribute.new(
        binding_prop_name,
        type: type_for_form_view(binding_prop_view),
        required: binding_prop_view.attrs.has?(:required)
      )

      attributes << attribute
    end
  end
end
discover_form_scopes(view_path:, view: nil, parent_scope: nil, parent_exposure: nil) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 214
def discover_form_scopes(view_path:, view: nil, parent_scope: nil, parent_exposure: nil)
  view.each_binding_scope do |binding_scope_node|
    if binding_scope_node.significant?(:field) || binding_scope_node.find_significant_nodes(:field).any?
      binding_scope_view = Presenter::View.from_object(binding_scope_node)
      scope = scope_for_binding(binding_scope_view.binding_name, parent_scope)

      # Discover attributes from scopes nested within forms.
      #
      discover_attributes(binding_scope_view).each do |attribute|
        unless scope.attribute(attribute.name, type: :form)
          scope.add_attribute(attribute, type: :form)
        end
      end

      # Discover nested form scopes.
      #
      discover_form_scopes(
        view_path: view_path,
        view: binding_scope_view,
        parent_scope: scope
      )
    end
  end
end
discover_nested(view) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 266
def discover_nested(view)
  view.binding_scopes.select { |binding_scope_node|
    binding_scope_node.significant?(:field) || binding_scope_node.find_significant_nodes(:field).any?
  }.map { |binding_scope_node|
    binding_scope_view = Presenter::View.from_object(binding_scope_node)

    Nested.new(
      binding_scope_view.binding_name,
      attributes: discover_attributes(binding_scope_view),
      nested: discover_nested(binding_scope_view)
    )
  }
end
discover_view_path_associations(view_path:) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 198
def discover_view_path_associations(view_path:)
  view_path_parts = view_path.split("/").reverse.map(&:to_sym)

  until view_path_parts.count < 2
    view_path_part = view_path_parts.shift

    if child_scope = scope(view_path_part)
      view_path_parts.map { |each_view_path_part|
        scope(each_view_path_part)
      }.compact.each do |parent_scope|
        child_scope.add_parent(parent_scope)
      end
    end
  end
end
discover_view_scopes(view_path:, view: nil, parent_scope: nil, parent_exposure: nil, binding_path: []) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 49
def discover_view_scopes(view_path:, view: nil, parent_scope: nil, parent_exposure: nil, binding_path: [])
  unless view
    composer = Presenter::Composers::View.new(view_path, app: @app)
    view = composer.view(return_cached: true)
  end

  # Descend to find the most specific scope first.
  #
  view.each_binding_scope do |binding_scope_node|
    unless binding_scope_node.significant?(:within_form) || binding_scope_node.labeled?(:plug)
      binding_scope_view = Presenter::View.from_object(binding_scope_node)
      scope = scope_for_binding(binding_scope_view.binding_name, parent_scope)

      # Discover attributes from scopes nested within views.
      #
      discover_attributes(binding_scope_view, fields: false).each do |attribute|
        unless scope.attribute(attribute.name, type: :view)
          scope.add_attribute(attribute, type: :view)
        end
      end

      # Define an endpoint for this scope.
      #
      endpoint = ensure_endpoint(
        view_path, view.info.dig(:reflection, :endpoint)
      )

      exposure = endpoint.exposures.find { |e|
        e.binding == binding_scope_view.channeled_binding_name && e.parent.equal?(parent_exposure)
      }

      unless exposure
        exposure = Exposure.new(
          scope: scope,
          node: binding_scope_node,
          parent: parent_exposure,
          binding: binding_scope_view.channeled_binding_name,
          dataset: binding_scope_view.label(:dataset)
        )

        endpoint.add_exposure(exposure)
      end

      # Discover nested view scopes.
      #
      discover_view_scopes(
        view_path: view_path,
        view: binding_scope_view,
        parent_scope: scope,
        parent_exposure: exposure,
        binding_path: binding_path.dup << binding_scope_node.label(:binding)
      )
    end
  end

  # Discover forms.
  #
  view.object.each_significant_node(:form) do |form_node|
    if form_node.labeled?(:binding) && !form_node.labeled?(:plug)
      form_view = Presenter::View.from_object(form_node)
      scope = scope_for_binding(form_view.binding_name, parent_scope)

      # Discover attributes from scopes nested within forms.
      #
      attributes = discover_attributes(form_view)
      attributes.each do |attribute|
        unless scope.attribute(attribute.name, type: :form)
          scope.add_attribute(attribute, type: :form)
        end
      end

      # Define an endpoint for this form.
      #
      endpoint = ensure_endpoint(
        view_path, view.info.dig(:reflection, :endpoint)
      )

      unless endpoint.exposures.any? { |e| e.binding == form_view.channeled_binding_name }
        exposure = Exposure.new(
          scope: scope,
          node: form_node,
          parent: parent_exposure,
          binding: form_view.channeled_binding_name
        )

        endpoint.add_exposure(exposure)

        # Define the reflected action, if there is one.
        #
        if action = action_for_form(form_view, view_path)
          # Define an action to handle this form submission.
          #
          scope.actions << Action.new(
            name: action,
            scope: scope,
            node: form_node,

            # We need the view path to to identify the correct action to pull
            # expected attributes from on submission.
            #
            view_path: view_path,

            # We need the channeled binding name to differentiate between submissions of two
            # forms with the same scope from the same view path.
            #
            binding: form_view.label(:channeled_binding),

            attributes: attributes,
            nested: discover_nested(form_view),
            parents: binding_path.map { |binding_path_part|
              scope(binding_path_part)
            }
          )
        end

        # Discover nested form scopes.
        discover_form_scopes(
          view_path: view_path,
          view: form_view,
          parent_scope: scope,
          parent_exposure: exposure
        )
      end
    end
  end

  # Define delete actions for delete links.
  #
  view.object.find_significant_nodes(:endpoint).select { |endpoint_node|
    endpoint_node.label(:endpoint).to_s.end_with?("_delete")
  }.reject { |endpoint_node|
    endpoint_node.label(:endpoint).to_s.start_with?("@")
  }.each do |endpoint_node|
    scope = scope_for_binding(
      endpoint_node.label(:endpoint).to_s.split("_", 2)[0],
      parent_scope
    )

    if scope && endpoint_node.label(:endpoint).to_s == "#{scope.plural_name}_delete" && !scope.action(:delete)
      scope.actions << Action.new(
        name: :delete,
        scope: scope,
        node: endpoint_node,
        view_path: view_path
      )
    end
  end
end
ensure_endpoint(view_path, options) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 340
def ensure_endpoint(view_path, options)
  unless endpoint = @endpoints.find { |e| e.view_path == view_path }
    endpoint = Endpoint.new(view_path, options: options)
    @endpoints << endpoint
  end

  endpoint
end
scope_for_binding(binding, parent_scope) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 280
def scope_for_binding(binding, parent_scope)
  unless scope = scopes.find { |possible_scope| possible_scope.named?(binding) }
    scope = Scope.new(binding); @scopes << scope
  end

  if parent_scope
    scope.add_parent(parent_scope)
  end

  scope
end
type_for_attribute_type(type) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 305
def type_for_attribute_type(type)
  case type
  when "date"
    :date
  when "time"
    :time
  when "datetime-local"
    :datetime
  when "number", "range"
    :decimal
  else
    nil
  end
end
type_for_binding_name(binding_name) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 298
def type_for_binding_name(binding_name)
  if binding_name.end_with?("_at")
    :datetime
  else
  end
end
type_for_form_view(view) click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 292
def type_for_form_view(view)
  type_for_binding_name(view.binding_name.to_s) ||
  (view.attributes.has?(:type) && type_for_attribute_type(view.attributes[:type])) ||
  :string
end
view_paths() click to toggle source
# File lib/pakyow/reflection/mirror.rb, line 43
def view_paths
  @app.state(:templates).reject { |template_store|
    @app.config.reflection.ignored_template_stores.include?(template_store.name)
  }.flat_map(&:paths)
end