Skip to content

Commit

Permalink
Refactor data extension (#2221)
Browse files Browse the repository at this point in the history
* Refactor data extension

* Things deserve their own files

* Lint
  • Loading branch information
tdreyno committed Nov 19, 2018
1 parent be66779 commit bb7a9ad
Show file tree
Hide file tree
Showing 10 changed files with 354 additions and 262 deletions.
2 changes: 1 addition & 1 deletion middleman-core/lib/middleman-core/core_extensions.rb
Expand Up @@ -16,7 +16,7 @@
# to dynamic requests.
Middleman::Extensions.register :data, auto_activate: :before_sitemap do
require 'middleman-core/core_extensions/data'
Middleman::CoreExtensions::Data
Middleman::CoreExtensions::Data::DataExtension
end

# Catch and show exceptions at the Rack level
Expand Down
235 changes: 32 additions & 203 deletions middleman-core/lib/middleman-core/core_extensions/data.rb
@@ -1,224 +1,53 @@
require 'middleman-core/contracts'
require 'middleman-core/util/data'
require 'middleman-core/core_extensions/data/controller'

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
class Data < Extension
attr_reader :data_store
module Data
# The data extension parses YAML and JSON files in the `data/` directory
# and makes them available to `config.rb`, templates and extensions
class DataExtension < Extension
attr_reader :data_store

define_setting :data_dir, ENV['MM_DATA_DIR'] || 'data', 'The directory data files are stored in'
define_setting :data_dir, ENV['MM_DATA_DIR'] || 'data', 'The directory data files are stored in'

# Make the internal `data_store` method available as `app.data`
expose_to_application data: :data_store
# Make the internal `data_store` method available as `app.data`
expose_to_application data: :data_store

# Exposes `data` to templates
expose_to_template data: :data_store
# Exposes `data` to templates
expose_to_template data: :data_store

# The regex which tells Middleman which files are for data
DATA_FILE_MATCHER = /^(.*?)[\w-]+\.(yml|yaml|json)$/.freeze

def initialize(app, options_hash = ::Middleman::EMPTY_HASH, &block)
super

@data_store = DataStore.new(app, DATA_FILE_MATCHER)

start_watching(app.config[:data_dir])
end

def start_watching(dir)
@original_data_dir = dir

# Tell the file watcher to observe the :data_dir
@watcher = app.files.watch :data,
path: File.expand_path(dir, app.root),
only: DATA_FILE_MATCHER

# Setup data files before anything else so they are available when
# parsing config.rb
app.files.on_change(:data, &@data_store.method(:update_files))
end

def after_configuration
return unless @original_data_dir != app.config[:data_dir]

@watcher.update_path(app.config[:data_dir])
end

# The core logic behind the data extension.
class DataStore
include Contracts

# Setup data store
#
# @param [Middleman::Application] app The current instance of Middleman
def initialize(app, data_file_matcher)
@app = app
@data_file_matcher = data_file_matcher
@local_data = {}
@local_data_enhanced = nil
@local_sources = {}
@callback_sources = {}
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]
Contract Symbol, Or[Hash, Array] => Hash
def store(name = nil, content = nil)
@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]
Contract Maybe[Symbol], Maybe[Proc] => Hash
def callbacks(name = nil, proc = nil)
@callback_sources[name.to_s] = proc unless name.nil? || proc.nil?
@callback_sources
end

Contract ArrayOf[IsA['Middleman::SourceFile']], ArrayOf[IsA['Middleman::SourceFile']] => Any
def update_files(updated_files, removed_files)
updated_files.each(&method(:touch_file))
removed_files.each(&method(:remove_file))

@app.sitemap.rebuild_resource_list!(:touched_data_file)
end

# Update the internal cache for a given file path
#
# @param [String] file The file to be re-parsed
# @return [void]
Contract IsA['Middleman::SourceFile'] => Any
def touch_file(file)
data_path = file[:relative_path]
extension = File.extname(data_path)
basename = File.basename(data_path, extension)

return unless %w[.yaml .yml .json].include?(extension)

if %w[.yaml .yml].include?(extension)
data, postscript = ::Middleman::Util::Data.parse(file, @app.config[:frontmatter_delims], :yaml)
data[:postscript] = postscript if !postscript.nil? && data.is_a?(Hash)
elsif extension == '.json'
data, _postscript = ::Middleman::Util::Data.parse(file, @app.config[:frontmatter_delims], :json)
end

data_branch = @local_data

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

data_branch[basename] = data

@local_data_enhanced = nil
end

# Remove a given file from the internal cache
#
# @param [String] file The file to be cleared
# @return [void]
Contract IsA['Middleman::SourceFile'] => Any
def remove_file(file)
data_path = file[:relative_path]
extension = File.extname(data_path)
basename = File.basename(data_path, extension)

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.key?(basename)

@local_data_enhanced = nil
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]
Contract Or[String, Symbol] => Maybe[Or[Array, IsA['Middleman::Util::EnhancedHash']]]
def data_for_path(path)
response = if store.key?(path.to_s)
store[path.to_s]
elsif callbacks.key?(path.to_s)
callbacks[path.to_s].call
end

::Middleman::Util.recursively_enhance(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.key?(path.to_s)
# Any way to cache this?
@local_data_enhanced ||= ::Middleman::Util.recursively_enhance(@local_data)
return @local_data_enhanced[path.to_s]
else
result = data_for_path(path)
return result if result
end
# The regex which tells Middleman which files are for data
DATA_FILE_MATCHER = /^(.*?)[\w-]+\.(yml|yaml|json)$/.freeze

Contract IsA['::Middleman::Application'], Hash => Any
def initialize(app, options_hash = ::Middleman::EMPTY_HASH, &block)
super
end

# Needed so that method_missing makes sense
def respond_to?(method, include_private = false)
super || 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 key?(key)
end
@data_store = DataStoreController.new(app)

def key?(key)
string_key = key.to_s
@local_data.key?(string_key) || @local_sources.key?(string_key) || @callback_sources.key?(string_key)
start_watching(app.config[:data_dir])
end

alias has_key? key?
Contract String => Any
def start_watching(dir)
@original_data_dir = dir

# Convert all the data into a static hash
#
# @return [Hash]
Contract Hash
def to_h
data = {}
# Tell the file watcher to observe the :data_dir
@watcher = app.files.watch :data,
path: File.expand_path(dir, app.root),
only: DATA_FILE_MATCHER

store.each_key do |k|
data[k] = data_for_path(k)
end

callbacks.each_key do |k|
data[k] = data_for_path(k)
end
# Setup data files before anything else so they are available when
# parsing config.rb
app.files.on_change(:data, &@data_store.method(:update_files))
end

(@local_data || {}).each do |k, v|
data[k] = v
end
Contract Any
def after_configuration
return unless @original_data_dir != app.config[:data_dir]

data
@watcher.update_path(app.config[:data_dir])
end
end
end
Expand Down
@@ -0,0 +1,86 @@
require 'middleman-core/util/data'
require 'middleman-core/core_extensions/data/stores/local_file'
require 'middleman-core/core_extensions/data/stores/static'
require 'middleman-core/core_extensions/data/stores/callback'

module Middleman
module CoreExtensions
module Data
# The core logic behind the data extension.
class DataStoreController
extend Forwardable

def_delegator :@local_file_data_store, :update_files
def_delegator :@static_data_store, :store
def_delegator :@callback_data_store, :callbacks

def initialize(app)
@local_file_data_store = Data::Stores::LocalFileDataStore.new(app)
@static_data_store = Data::Stores::StaticDataStore.new
@callback_data_store = Data::Stores::CallbackDataStore.new

# Sorted in order of access precedence.
@data_stores = [
@local_file_data_store,
@static_data_store,
@callback_data_store
]

@enhanced_cache = {}
end

def key?(k)
@data_stores.any? { |s| s.key?(k) }
end
alias has_key? key?

def key(k)
source = @data_stores.find { |s| s.key?(k) }
source[k] unless source.nil?
end

def enhanced_key(k)
value = key(k)

if @enhanced_cache.key?(k)
cached_id, cached_value = @enhanced_cache[k]

return cached_value if cached_id == value.object_id

@enhanced_cache.delete(k)
end

enhanced = ::Middleman::Util.recursively_enhance(value)

@enhanced_cache[k] = [value.object_id, enhanced]

enhanced
end

# "Magically" find namespaces of data if they exist
#
# @param [String] path The namespace to search for
# @return [Hash, nil]
def method_missing(method)
return enhanced_key(method) if key?(method)

super
end

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

# Convert all the data into a static hash
#
# @return [Hash]
def to_h
@data_stores.reduce({}) do |sum, store|
sum.merge(store.to_h)
end
end
end
end
end
end
@@ -0,0 +1,35 @@
require 'middleman-core/contracts'

module Middleman
module CoreExtensions
module Data
module Stores
class BaseDataStore
include Contracts

Contract Symbol => Bool
def key?(_k)
raise NotImplementedError
end

Contract Symbol => Or[Array, Hash]
def [](_k)
raise NotImplementedError
end

Contract ArrayOf[Symbol]
def keys
raise NotImplementedError
end

Contract Hash
def to_h
keys.each_with_object({}) do |k, sum|
sum[k] = self[k]
end
end
end
end
end
end
end

0 comments on commit bb7a9ad

Please sign in to comment.