diff --git a/middleman-core/lib/middleman-core/application.rb b/middleman-core/lib/middleman-core/application.rb index fd072d57..1121bf23 100644 --- a/middleman-core/lib/middleman-core/application.rb +++ b/middleman-core/lib/middleman-core/application.rb @@ -15,6 +15,8 @@ require 'hooks' # Our custom logger require 'middleman-core/logger' +require 'middleman-core/contracts' + require 'middleman-core/sitemap/store' require 'middleman-core/configuration' @@ -30,6 +32,7 @@ require 'middleman-core/template_renderer' module Middleman class Application extend Forwardable + include Contracts class << self # Global configuration for the whole Middleman project. @@ -165,7 +168,11 @@ module Middleman attr_reader :config attr_reader :generic_template_context attr_reader :extensions + + Contract None => SetOf['Middleman::Application::MiddlewareDescriptor'] attr_reader :middleware + + Contract None => SetOf['Middleman::Application::MapDescriptor'] attr_reader :mappings # Reference to Logger singleton @@ -179,8 +186,8 @@ module Middleman # Search the root of the project for required files $LOAD_PATH.unshift(root) unless $LOAD_PATH.include?(root) - @middleware = [] - @mappings = [] + @middleware = Set.new + @mappings = Set.new @template_context_class = Class.new(Middleman::TemplateContext) @generic_template_context = @template_context_class.new(self) @@ -296,20 +303,26 @@ module Middleman File.join(root, config[:source]) end + MiddlewareDescriptor = Struct.new(:class, :options, :block) + # Use Rack middleware # # @param [Class] middleware Middleware module # @return [void] + Contract Any, Args[Any], Proc => Any def use(middleware, *args, &block) - @middleware << [middleware, args, block] + @middleware << MiddlewareDescriptor.new(middleware, args, block) end + MapDescriptor = Struct.new(:path, :block) + # Add Rack App mapped to specific path # # @param [String] map Path to map # @return [void] + Contract String, Proc => Any def map(map, &block) - @mappings << [map, block] + @mappings << MapDescriptor.new(map, block) end # Work around this bug: http://bugs.ruby-lang.org/issues/4521 diff --git a/middleman-core/lib/middleman-core/contracts.rb b/middleman-core/lib/middleman-core/contracts.rb index 3eec15d2..2f13bc1c 100644 --- a/middleman-core/lib/middleman-core/contracts.rb +++ b/middleman-core/lib/middleman-core/contracts.rb @@ -1,22 +1,56 @@ if ENV['TEST'] || ENV['CONTRACTS'] == 'true' require 'contracts' - class IsA - def self.[](val) - @lookup ||= {} - @lookup[val] ||= new(val) + module Contracts + class IsA + def self.[](val) + @lookup ||= {} + @lookup[val] ||= new(val) + end + + def initialize(val) + @val = val + end + + def valid?(val) + val.is_a? @val.constantize + end end - def initialize(val) - @val = val + class ArrayOf + def initialize(contract) + @contract = contract.is_a?(String) ? IsA[contract] : contract + end end - def valid?(val) - val.is_a? @val.constantize + class SetOf < CallableClass + def initialize(contract) + @contract = contract.is_a?(String) ? IsA[contract] : contract + end + + def valid?(vals) + return false unless vals.is_a?(Set) + vals.all? do |val| + res, _ = Contract.valid?(val, @contract) + res + end + end + + def to_s + "a set of #{@contract}" + end + + def testable? + Testable.testable? @contract + end + + def test_data + Set.new([], [Testable.test_data(@contract)], [Testable.test_data(@contract), Testable.test_data(@contract)]) + end end + + ResourceList = Contracts::ArrayOf[IsA['Middleman::Sitemap::Resource']] end - - ResourceList = Contracts::ArrayOf[IsA['Middleman::Sitemap::Resource']] else module Contracts def self.included(base) @@ -91,5 +125,12 @@ else class IsA < Callable end + + class SetOf < Callable + end end end + +module Contracts + PATH_MATCHER = Or[String, RespondTo[:match], RespondTo[:call], RespondTo[:to_s]] +end diff --git a/middleman-core/lib/middleman-core/core_extensions/default_helpers.rb b/middleman-core/lib/middleman-core/core_extensions/default_helpers.rb index 99120528..fa359a77 100644 --- a/middleman-core/lib/middleman-core/core_extensions/default_helpers.rb +++ b/middleman-core/lib/middleman-core/core_extensions/default_helpers.rb @@ -140,7 +140,7 @@ class Middleman::CoreExtensions::DefaultHelpers < ::Middleman::Extension path << index_file if path.end_with?('/') path = ::Middleman::Util.strip_leading_slash(path) - classes = [] + classes = Set.new parts = path.split('.').first.split('/') parts.each_with_index { |_, i| classes << parts.first(i + 1).join('_') } diff --git a/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb b/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb index 68a83280..fca753ab 100644 --- a/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb +++ b/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb @@ -55,28 +55,30 @@ module Middleman @app = app @known_paths = Set.new - @_changed = [] - @_deleted = [] + @on_change_callbacks = Set.new + @on_delete_callbacks = Set.new end + CallbackDescriptor = Struct.new(:proc, :matcher) + # Add callback to be run on file change # # @param [nil,Regexp] matcher A Regexp to match the change path against # @return [Array] - Contract Or[Regexp, Proc] => ArrayOf[ArrayOf[Or[Proc, Regexp, nil]]] + Contract Or[Regexp, Proc] => SetOf['Middleman::CoreExtensions::FileWatcher::API::CallbackDescriptor'] def changed(matcher=nil, &block) - @_changed << [block, matcher] if block_given? - @_changed + @on_change_callbacks << CallbackDescriptor.new(block, matcher) if block_given? + @on_change_callbacks end # Add callback to be run on file deletion # # @param [nil,Regexp] matcher A Regexp to match the deleted path against # @return [Array] - Contract Or[Regexp, Proc] => ArrayOf[ArrayOf[Or[Proc, Regexp, nil]]] + Contract Or[Regexp, Proc] => SetOf['Middleman::CoreExtensions::FileWatcher::API::CallbackDescriptor'] def deleted(matcher=nil, &block) - @_deleted << [block, matcher] if block_given? - @_deleted + @on_delete_callbacks << CallbackDescriptor.new(block, matcher) if block_given? + @on_delete_callbacks end # Notify callbacks that a file changed @@ -159,9 +161,9 @@ module Middleman # @return [void] def run_callbacks(path, callbacks_name) path = path.to_s - send(callbacks_name).each do |callback, matcher| - next unless matcher.nil? || path.match(matcher) - @app.instance_exec(path, &callback) + send(callbacks_name).each do |callback| + next unless callback[:matcher].nil? || path.match(callback[:matcher]) + @app.instance_exec(path, &callback[:proc]) end end end diff --git a/middleman-core/lib/middleman-core/core_extensions/routing.rb b/middleman-core/lib/middleman-core/core_extensions/routing.rb index 61e80028..2bf11eea 100644 --- a/middleman-core/lib/middleman-core/core_extensions/routing.rb +++ b/middleman-core/lib/middleman-core/core_extensions/routing.rb @@ -9,7 +9,7 @@ module Middleman def initialize(app, options_hash={}, &block) super - @page_configs = [] + @page_configs = Set.new end def before_configuration @@ -20,12 +20,14 @@ module Middleman Contract ResourceList => ResourceList def manipulate_resource_list(resources) resources.each do |resource| - @page_configs.each do |matcher, metadata| - resource.add_metadata(metadata) if Middleman::Util.path_match(matcher, "/#{resource.path}") + @page_configs.each do |p| + resource.add_metadata(p[:metadata]) if Middleman::Util.path_match(p[:path], "/#{resource.path}") end end end + PageDescriptor = Struct.new(:path, :metadata) + # The page method allows options to be set for a given source path, regex, or glob. # Options that may be set include layout, locals, proxy, andx ignore. # @@ -43,6 +45,7 @@ module Middleman # @option opts [Hash] locals Local variables for the template. These will be available when the template renders. # @option opts [Hash] data Extra metadata to add to the page. This is the same as frontmatter, though frontmatter will take precedence over metadata defined here. Available via {Resource#data}. # @return [void] + Contract String, Hash => Any def page(path, opts={}) options = opts.dup @@ -63,7 +66,7 @@ module Middleman path = '/' + Util.strip_leading_slash(path) if path.is_a?(String) - @page_configs << [path, metadata] + @page_configs << PageDescriptor.new(path, metadata) end end end diff --git a/middleman-core/lib/middleman-core/extensions.rb b/middleman-core/lib/middleman-core/extensions.rb index 37690957..77c99a64 100644 --- a/middleman-core/lib/middleman-core/extensions.rb +++ b/middleman-core/lib/middleman-core/extensions.rb @@ -9,9 +9,9 @@ module Middleman @registered = {} @auto_activate = { # Activate before the Sitemap is instantiated - before_sitemap: [], + before_sitemap: Set.new, # Activate the extension before `config.rb` and the `:before_configuration` hook. - before_configuration: [] + before_configuration: Set.new } AutoActivation = Struct.new(:name, :modes) diff --git a/middleman-core/lib/middleman-core/extensions/minify_css.rb b/middleman-core/lib/middleman-core/extensions/minify_css.rb index 08a28d95..75b2ca4b 100644 --- a/middleman-core/lib/middleman-core/extensions/minify_css.rb +++ b/middleman-core/lib/middleman-core/extensions/minify_css.rb @@ -32,6 +32,11 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension # Init # @param [Class] app # @param [Hash] options + Contract RespondTo[:call], ({ + ignore: ArrayOf[PATH_MATCHER], + inline: Bool, + compressor: Or[Proc, RespondTo[:to_proc], RespondTo[:compress]] + }) => Any def initialize(app, options={}) @app = app @ignore = options.fetch(:ignore) diff --git a/middleman-core/lib/middleman-core/extensions/minify_javascript.rb b/middleman-core/lib/middleman-core/extensions/minify_javascript.rb index 208b4fda..5c90bd13 100644 --- a/middleman-core/lib/middleman-core/extensions/minify_javascript.rb +++ b/middleman-core/lib/middleman-core/extensions/minify_javascript.rb @@ -23,6 +23,11 @@ class Middleman::Extensions::MinifyJavascript < ::Middleman::Extension # Init # @param [Class] app # @param [Hash] options + Contract RespondTo[:call], ({ + ignore: ArrayOf[PATH_MATCHER], + inline: Bool, + compressor: Or[Proc, RespondTo[:to_proc], RespondTo[:compress]] + }) => Any def initialize(app, options={}) @app = app @ignore = options.fetch(:ignore) diff --git a/middleman-core/lib/middleman-core/middleware/inline_url_rewriter.rb b/middleman-core/lib/middleman-core/middleware/inline_url_rewriter.rb index 094a9d9c..1151800c 100644 --- a/middleman-core/lib/middleman-core/middleware/inline_url_rewriter.rb +++ b/middleman-core/lib/middleman-core/middleware/inline_url_rewriter.rb @@ -8,21 +8,31 @@ module Middleman class InlineURLRewriter include Contracts + IGNORE_DESCRIPTOR = Or[Regexp, RespondTo[:call], String] + + Contract RespondTo[:call], ({ + middleman_app: IsA['Middleman::Application'], + id: Maybe[Symbol], + proc: Or[Proc, Method], + url_extensions: ArrayOf[String], + source_extensions: ArrayOf[String], + ignore: ArrayOf[IGNORE_DESCRIPTOR] + }) => Any def initialize(app, options={}) @rack_app = app - @middleman_app = options[:middleman_app] + @middleman_app = options.fetch(:middleman_app) - @uid = options[:id] - @proc = options[:proc] + @uid = options.fetch(:id, nil) + @proc = options.fetch(:proc) raise 'InlineURLRewriter requires a :proc to call with inline URL results' unless @proc - @exts = options[:url_extensions] + @exts = options.fetch(:url_extensions) - @source_exts = options[:source_extensions] + @source_exts = options.fetch(:source_extensions) @source_exts_regex_text = Regexp.union(@source_exts).to_s - @ignore = options[:ignore] + @ignore = options.fetch(:ignore) end def call(env) @@ -66,7 +76,7 @@ module Middleman [status, headers, response] end - Contract Or[Regexp, RespondTo[:call], String] => Bool + Contract IGNORE_DESCRIPTOR => Bool def should_ignore?(validator, value) if validator.is_a? Regexp # Treat as Regexp diff --git a/middleman-core/lib/middleman-core/rack.rb b/middleman-core/lib/middleman-core/rack.rb index 3d540685..cd80f61b 100644 --- a/middleman-core/lib/middleman-core/rack.rb +++ b/middleman-core/lib/middleman-core/rack.rb @@ -23,15 +23,15 @@ module Middleman app.use ::Rack::Lint app.use ::Rack::Head - @middleman.middleware.each do |klass, options, middleware_block| - app.use(klass, *options, &middleware_block) + @middleman.middleware.each do |middleware| + app.use(middleware[:class], *middleware[:options], &middleware[:block]) end inner_app = self app.map('/') { run inner_app } - @middleman.mappings.each do |path, map_block| - app.map(path, &map_block) + @middleman.mappings.each do |mapping| + app.map(mapping[:path], &mapping[:block]) end app diff --git a/middleman-core/lib/middleman-core/sitemap/extensions/ignores.rb b/middleman-core/lib/middleman-core/sitemap/extensions/ignores.rb index 7ac26615..565c3151 100644 --- a/middleman-core/lib/middleman-core/sitemap/extensions/ignores.rb +++ b/middleman-core/lib/middleman-core/sitemap/extensions/ignores.rb @@ -10,7 +10,7 @@ module Middleman @app.define_singleton_method :ignore, &method(:create_ignore) # Array of callbacks which can ass ignored - @ignored_callbacks = [] + @ignored_callbacks = Set.new @app.sitemap.define_singleton_method :ignored?, &method(:ignored?) end diff --git a/middleman-core/lib/middleman-core/sitemap/resource.rb b/middleman-core/lib/middleman-core/sitemap/resource.rb index eee2068a..add3919d 100644 --- a/middleman-core/lib/middleman-core/sitemap/resource.rb +++ b/middleman-core/lib/middleman-core/sitemap/resource.rb @@ -30,8 +30,11 @@ module Middleman # @return [String] alias_method :request_path, :destination_path + METADATA_HASH = ({ options: Maybe[Hash], locals: Maybe[Hash], page: Maybe[Hash] }) + # The metadata for this resource # @return [Hash] + Contract None => METADATA_HASH attr_reader :metadata # Initialize resource with parent store and URL @@ -66,7 +69,7 @@ module Middleman # Locals are local variables for rendering this resource's template # Page are data that is exposed through this resource's data member. # Note: It is named 'page' for backwards compatibility with older MM. - Contract Hash => Hash + Contract METADATA_HASH => METADATA_HASH def add_metadata(meta={}) @metadata.deep_merge!(meta) end diff --git a/middleman-core/lib/middleman-core/util.rb b/middleman-core/lib/middleman-core/util.rb index b98994d0..36dd881d 100644 --- a/middleman-core/lib/middleman-core/util.rb +++ b/middleman-core/lib/middleman-core/util.rb @@ -124,7 +124,7 @@ module Middleman # @param [String, #match, #call] matcher A matcher String, RegExp, Proc, etc. # @param [String] path A path as a string # @return [Boolean] Whether the path matches the matcher - Contract Or[String, RespondTo[:match], RespondTo[:call], RespondTo[:to_s]], String => Bool + Contract PATH_MATCHER, String => Bool def self.path_match(matcher, path) case when matcher.is_a?(String)