sitepress/sitepress

View on GitHub
sitepress-core/lib/sitepress/resource.rb

Summary

Maintainability
A
35 mins
Test Coverage
require "forwardable"

module Sitepress
  # Represents the request path of an asset. There may be multiple
  # resources that point to the same asset. Resources are immutable
  # and may be altered by the resource proxy.
  class Resource
    extend Forwardable
    def_delegators :asset, :body, :data, :renderable?

    attr_reader :node, :asset

    attr_accessor :format, :mime_type, :handler

    # Default scope for querying parent/child/sibling resources.
    DEFAULT_FILTER_SCOPE = :same

    def initialize(asset:, node:, format: nil, mime_type: nil, handler: nil)
      @asset = asset
      @node = node
      @format = format || asset.format
      @mime_type = mime_type || asset.mime_type
      @handler = handler || asset.handler
    end

    def request_path
      File.join("/", *lineage, request_filename)
    end

    # Eugh, I really don't like this because it's not a full URL. To get a full URL though this thing
    # needs to be put into `url_for(request_path)` in Rails to get the hostname. I don't want to inject
    # that dependency into this thing, so here it is.
    alias :url :request_path

    def node=(destination)
      if destination.resources.format? format
        raise Sitepress::Error, "#{destination.inspect} already has a resource with a #{format} format"
      end
      remove
      destination.resources.add self
      @node = destination
    end

    # Moves the resource to a destination node. Moving a resource to a Sitepress::Node
    # is a little weird for people who are accustomed to working with files, which is pretty
    # much everybody (including myself). A child node has to be created on the destination node
    # with the name of the resource node.
    #
    # Or just ignore all of that and use the `move_to` method so you can feel like you're
    # moving files around.
    def move_to(destination)
      raise Sitepress::Error, "#{destination.inspect} is not a Sitepress::Node" unless destination.is_a? Sitepress::Node
      self.tap do |resource|
        resource.node = destination.child(node.name)
      end
    end

    # Creates a duplicate of the resource and moves it to the destination.
    def copy_to(destination)
      raise Sitepress::Error, "#{destination.inspect} is not a Sitepress::Node" unless destination.is_a? Sitepress::Node
      self.clone.tap do |resource|
        resource.node = destination.child(node.name)
      end
    end

    # Clones should be initialized with a nil node. Initializing with a node would mean that multiple resources
    # are pointing to the same node, which shouldn't be possible.
    def clone
      self.class.new(asset: @asset, node: nil, format: @format, mime_type: @mime_type, handler: @handler)
    end

    # Removes the resource from the node's resources list.
    def remove
      node.resources.remove format if node
    end

    def inspect
      "<#{self.class}:#{object_id} request_path=#{request_path.inspect} asset_path=#{asset.path.to_s.inspect}>"
    end

    def parent(**args)
      parents(**args).first
    end

    def parents(**args)
      filter_resources(**args){ node.parents }
    end

    def siblings(**args)
      filter_resources(**args){ node.siblings }.compact
    end

    def children(**args)
      filter_resources(**args){ node.children }.compact
    end

    def ==(resource)
      resource.request_path == request_path
    end

    # Used internally to construct paths from the current node up to the root node.
    def lineage
      node.parents.reject(&:root?).reverse.map(&:name)
    end

    class << self
      attr_accessor :path_suffix_hack_that_you_should_not_use

      def path_suffix_hack_that_you_should_not_use
        @path_suffix_hack_that_you_should_not_use ||= ""
      end
    end

    private
    # Filters parent/child/sibling resources by a type. The default behavior is to only return
    # resources of the same type. For example given the pages `/a.html`, `/a.gif`, `/a/b.html`,
    # if you query the parent from page `/a/b.html` you'd only get `/a.html` by default. If you
    # query the parents via `parents(type: :all)` you'd get get [`/a.html`, `/a.gif`]
    #
    # TODO: When `type: :all` is scoped, some queries will mistakenly return single resources.
    # :all should return an array of arrays to accurately represention levels.
    #
    # TODO: Put a better extension/mime_type handler into resource tree, then instead of faltening
    # below and select, we could call a single map and pull out a resources
    def filter_resources(type: DEFAULT_FILTER_SCOPE, &block)
      return [] unless node
      nodes = block.call

      case type
      when :all
        nodes.map{ |node| node.resources }
      when :same
        nodes.map{ |n| n.resources.format(format) }.flatten
      when String, Symbol, NilClass
        nodes.map{ |n| n.resources.format(type) }.flatten
      when MIME::Type
        nodes.map{ |n| n.resources.mime_type(type) }.flatten
      else
        raise ArgumentError, "Invalid type argument #{type}. Must be either :same, :all, an extension string, or a Mime::Type"
      end
    end

    # Deals with situations, particularly in the root node and other "index" nodes, for the `request_path`
    def request_filename
      if node.root? and node.default_format == format
        ""
      elsif node.root? and format
        "#{node.default_name}.#{format}"
      elsif node.root?
        "#{node.default_name}#{self.class.path_suffix_hack_that_you_should_not_use}"
      elsif format.nil? or node.default_format == format
        "#{node.name}#{self.class.path_suffix_hack_that_you_should_not_use}"
      else
        "#{node.name}.#{format}"
      end
    end
  end
end