lib/middleman-core/core_extensions/data.rb



module Middleman
  module CoreExtensions

    # The data extension parses YAML and JSON files in the data/ directory
    # and makes them available to config.rb, templates and extensions
    module Data

      # Extension registered
      class << self
        # @private
        def registered(app)
          # Data formats
          require 'yaml'
          require 'active_support/json'

          app.config.define_setting :data_dir, 'data', 'The directory data files are stored in'
          app.send :include, InstanceMethods
        end
        alias :included :registered
      end

      # Instance methods
      module InstanceMethods
        # Setup data files before anything else so they are available when
        # parsing config.rb
        def initialize
          self.files.changed DataStore.matcher do |file|
            self.data.touch_file(file) if file.start_with?("#{config[:data_dir]}/")
          end

          self.files.deleted DataStore.matcher do |file|
            self.data.remove_file(file) if file.start_with?("#{config[:data_dir]}/")
          end

          super
        end

        # The data object
        #
        # @return [DataStore]
        def data
          @_data ||= DataStore.new(self)
        end
      end

      # The core logic behind the data extension.
      class DataStore

        # Static methods
        class << self

          # The regex which tells Middleman which files are for data
          #
          # @return [Regexp]
          def matcher
            %r{[\w-]+\.(yml|yaml|json)$}
          end
        end

        # Store static data hash
        #
        # @param [Symbol] name Name of the data, used for namespacing
        # @param [Hash] content The content for this data
        # @return [Hash]
        def store(name=nil, content=nil)
          @_local_sources ||= {}
          @_local_sources[name.to_s] = content unless name.nil? || content.nil?
          @_local_sources
        end

        # Store callback-based data
        #
        # @param [Symbol] name Name of the data, used for namespacing
        # @param [Proc] proc The callback which will return data
        # @return [Hash]
        def callbacks(name=nil, proc=nil)
          @_callback_sources ||= {}
          @_callback_sources[name.to_s] = proc unless name.nil? || proc.nil?
          @_callback_sources
        end

        # Setup data store
        #
        # @param [Middleman::Application] app The current instance of Middleman
        def initialize(app)
          @app = app
          @local_data = {}
        end

        # Update the internal cache for a given file path
        #
        # @param [String] file The file to be re-parsed
        # @return [void]
        def touch_file(file)
          root = Pathname(@app.root)
          full_path = root + file
          extension = File.extname(file)
          basename  = File.basename(file, extension)

          data_path = full_path.relative_path_from(root + @app.config[:data_dir])

          if %w(.yaml .yml).include?(extension)
            data = YAML.load_file(full_path)
          elsif extension == '.json'
            data = ActiveSupport::JSON.decode(full_path.read)
          else
            return
          end

          data_branch = @local_data

          path = data_path.to_s.split(File::SEPARATOR)[0..-2]
          path.each do |dir|
            data_branch[dir] ||= ::Middleman::Util.recursively_enhance({})
            data_branch = data_branch[dir]
          end

          data_branch[basename] = ::Middleman::Util.recursively_enhance(data)
        end

        # Remove a given file from the internal cache
        #
        # @param [String] file The file to be cleared
        # @return [void]
        def remove_file(file)
          root = Pathname(@app.root)
          full_path = root + file
          extension = File.extname(file)
          basename  = File.basename(file, extension)

          data_path = full_path.relative_path_from(root + @app.config[:data_dir])

          data_branch = @local_data

          path = data_path.to_s.split(File::SEPARATOR)[0..-2]
          path.each do |dir|
            data_branch = data_branch[dir]
          end

          data_branch.delete(basename) if data_branch.has_key?(basename)
        end

        # Get a hash from either internal static data or a callback
        #
        # @param [String, Symbol] path The name of the data namespace
        # @return [Hash, nil]
        def data_for_path(path)
          response = nil

          @@local_sources ||= {}
          @@callback_sources ||= {}

          if self.store.has_key?(path.to_s)
            response = self.store[path.to_s]
          elsif self.callbacks.has_key?(path.to_s)
            response = self.callbacks[path.to_s].call()
          end

          response
        end

        # "Magically" find namespaces of data if they exist
        #
        # @param [String] path The namespace to search for
        # @return [Hash, nil]
        def method_missing(path)
          if @local_data.has_key?(path.to_s)
            return @local_data[path.to_s]
          else
            result = data_for_path(path)

            if result
              return ::Middleman::Util.recursively_enhance(result)
            end
          end

          super
        end

        # Needed so that method_missing makes sense
        def respond_to?(method, include_private = false)
          super || has_key?(method)
        end

        # Make DataStore act like a hash. Return requested data, or
        # nil if data does not exist
        #
        # @param [String, Symbol] key The name of the data namespace
        # @return [Hash, nil]
        def [](key)
          __send__(key) if has_key?(key)
        end

        def has_key?(key)
          @local_data.has_key?(key.to_s) || !!(data_for_path(key))
        end

        # Convert all the data into a static hash
        #
        # @return [Hash]
        def to_h
          data = {}

          self.store.each do |k, v|
            data[k] = data_for_path(k)
          end

          self.callbacks.each do |k, v|
            data[k] = data_for_path(k)
          end

          (@local_data || {}).each do |k, v|
            data[k] = v
          end

          data
        end
      end
    end
  end
end