From 22ce56492f63fe468a0c7fb68bfa3c389e48d13c Mon Sep 17 00:00:00 2001 From: Thomas Reynolds Date: Fri, 20 Mar 2015 13:58:23 -0700 Subject: [PATCH] Experiment with Hamster --- .../more-traversal-app/source/layout.erb | 2 +- .../fixtures/traversal-app/source/layout.erb | 2 +- .../middleman-core/core_extensions/data.rb | 4 +- .../core_extensions/front_matter.rb | 2 +- .../lib/middleman-core/sitemap/resource.rb | 5 +- middleman-core/lib/middleman-core/util.rb | 55 ++++++++---- .../util/hash_with_indifferent_access.rb | 86 ------------------- middleman-core/middleman-core.gemspec | 3 + .../spec/middleman-core/util_spec.rb | 18 ++-- 9 files changed, 57 insertions(+), 120 deletions(-) delete mode 100644 middleman-core/lib/middleman-core/util/hash_with_indifferent_access.rb diff --git a/middleman-core/fixtures/more-traversal-app/source/layout.erb b/middleman-core/fixtures/more-traversal-app/source/layout.erb index 205d5648..25411d39 100644 --- a/middleman-core/fixtures/more-traversal-app/source/layout.erb +++ b/middleman-core/fixtures/more-traversal-app/source/layout.erb @@ -16,6 +16,6 @@ Source: <%= current_page.source_file[:full_path].sub(root + "/", "") %> <% current_page.children.each do |p| %> <% if p.data %> - Data: <%= p.data %> + Data: <%= p.data.inspect %> <% end %> <% end %> \ No newline at end of file diff --git a/middleman-core/fixtures/traversal-app/source/layout.erb b/middleman-core/fixtures/traversal-app/source/layout.erb index 205d5648..25411d39 100644 --- a/middleman-core/fixtures/traversal-app/source/layout.erb +++ b/middleman-core/fixtures/traversal-app/source/layout.erb @@ -16,6 +16,6 @@ Source: <%= current_page.source_file[:full_path].sub(root + "/", "") %> <% current_page.children.each do |p| %> <% if p.data %> - Data: <%= p.data %> + Data: <%= p.data.inspect %> <% end %> <% end %> \ No newline at end of file diff --git a/middleman-core/lib/middleman-core/core_extensions/data.rb b/middleman-core/lib/middleman-core/core_extensions/data.rb index 3cb725c5..46279c43 100644 --- a/middleman-core/lib/middleman-core/core_extensions/data.rb +++ b/middleman-core/lib/middleman-core/core_extensions/data.rb @@ -162,8 +162,8 @@ module Middleman # @return [Hash, nil] def method_missing(path) if @local_data.key?(path.to_s) - @local_data[path.to_s] = ::Middleman::Util.recursively_enhance(@local_data[path.to_s]) - return @local_data[path.to_s] + # Any way to cache this? + return ::Middleman::Util.recursively_enhance(@local_data[path.to_s]) else result = data_for_path(path) return result if result diff --git a/middleman-core/lib/middleman-core/core_extensions/front_matter.rb b/middleman-core/lib/middleman-core/core_extensions/front_matter.rb index 5ef7f3fb..9f672a7e 100644 --- a/middleman-core/lib/middleman-core/core_extensions/front_matter.rb +++ b/middleman-core/lib/middleman-core/core_extensions/front_matter.rb @@ -83,7 +83,7 @@ module Middleman::CoreExtensions # Get the frontmatter and plain content from a file # @param [String] path - # @return [Array] + # @return [Array] Contract Pathname => [Hash, Maybe[String]] def frontmatter_and_content(full_path) data = {} diff --git a/middleman-core/lib/middleman-core/sitemap/resource.rb b/middleman-core/lib/middleman-core/sitemap/resource.rb index a0f9fee4..11a085eb 100644 --- a/middleman-core/lib/middleman-core/sitemap/resource.rb +++ b/middleman-core/lib/middleman-core/sitemap/resource.rb @@ -87,10 +87,9 @@ module Middleman end # Data about this resource, populated from frontmatter or extensions. - # @return [HashWithIndifferentAccess] - Contract IsA['Middleman::Util::HashWithIndifferentAccess'] + # @return [IndifferentHash] + Contract IsA['Middleman::Util::IndifferentHash'] def data - # TODO: Should this really be a HashWithIndifferentAccess? ::Middleman::Util.recursively_enhance(metadata[:page]) end diff --git a/middleman-core/lib/middleman-core/util.rb b/middleman-core/lib/middleman-core/util.rb index 679289a0..b13b8d7f 100644 --- a/middleman-core/lib/middleman-core/util.rb +++ b/middleman-core/lib/middleman-core/util.rb @@ -1,9 +1,6 @@ # For instrumenting require 'active_support/notifications' -# Indifferent hash access -require 'middleman-core/util/hash_with_indifferent_access' - # Core Pathname library used for traversal require 'pathname' @@ -14,6 +11,9 @@ require 'rack/mime' # DbC require 'middleman-core/contracts' +# Immutable Data +require 'hamster' + # For URI templating require 'addressable/uri' require 'addressable/template' @@ -76,27 +76,48 @@ module Middleman end end - # Recursively convert a normal Hash into a HashWithIndifferentAccess + class IndifferentHash < ::Hamster::Hash + def get(key) + key?(key.to_s) ? super(key.to_s) : super(key.to_sym) + end + + alias_method :method_missing, :get + end + + # Recursively convert a normal Hash into a IndifferentHash # # @private # @param [Hash] data Normal hash - # @return [Middleman::Util::HashWithIndifferentAccess] - FrozenDataStructure = Frozen[Or[HashWithIndifferentAccess, Array, String, TrueClass, FalseClass, Fixnum]] - Contract Maybe[Or[String, Array, Hash, HashWithIndifferentAccess]] => Maybe[FrozenDataStructure] - def recursively_enhance(data) - if data.is_a? HashWithIndifferentAccess - data - elsif data.is_a? Hash - HashWithIndifferentAccess.new(data) - elsif data.is_a? Array - data.map(&method(:recursively_enhance)).freeze - elsif data.frozen? || data.nil? || [::TrueClass, ::FalseClass, ::Fixnum].include?(data.class) - data + # @return [Middleman::Util::IndifferentHash] + FrozenDataStructure = Frozen[Or[IndifferentHash, Array, String, TrueClass, FalseClass, Fixnum]] + Contract Maybe[Or[String, Array, Hash, IndifferentHash]] => Maybe[FrozenDataStructure] + def recursively_enhance(obj) + case obj + when ::Hash + res = obj.map { |key, value| [recursively_enhance(key), recursively_enhance(value)] } + IndifferentHash.new(res) + when IndifferentHash + obj.map { |key, value| [recursively_enhance(key), recursively_enhance(value)] } + when ::Array + res = obj.map { |element| recursively_enhance(element) } + Hamster::Vector.new(res) + when ::SortedSet + # This clause must go before ::Set clause, since ::SortedSet is a ::Set. + res = obj.map { |element| recursively_enhance(element) } + Hamster::SortedSet.new(res) + when ::Set + res = obj.map { |element| recursively_enhance(element) } + Hamster::Set.new(res) + when Hamster::Vector, Hamster::Set, Hamster::SortedSet + obj.map { |element| recursively_enhance(element) } + when ::TrueClass, ::FalseClass, ::Fixnum, ::Symbol + obj else - data.dup.freeze + obj.dup.freeze end end + # Normalize a path to not include a leading slash # @param [String] path # @return [String] diff --git a/middleman-core/lib/middleman-core/util/hash_with_indifferent_access.rb b/middleman-core/lib/middleman-core/util/hash_with_indifferent_access.rb deleted file mode 100644 index 81aadcfd..00000000 --- a/middleman-core/lib/middleman-core/util/hash_with_indifferent_access.rb +++ /dev/null @@ -1,86 +0,0 @@ -require 'middleman-core/contracts' - -module Middleman - module Util - # A hash with indifferent access and magic predicates. - # Copied from Thor - # - # hash = Middleman::Util::HashWithIndifferentAccess.new 'foo' => 'bar', 'baz' => 'bee', 'force' => true - # - # hash[:foo] #=> 'bar' - # hash['foo'] #=> 'bar' - # hash.foo? #=> true - # - class HashWithIndifferentAccess < ::Hash #:nodoc: - include Contracts - - Contract Hash => Any - def initialize(hash={}) - super() - - hash.each do |key, val| - self[key] = Util.recursively_enhance(val) - end - - freeze - end - - def [](key) - super(convert_key(key)) - end - - def []=(key, value) - super(convert_key(key), value) - end - - def delete(key) - super(convert_key(key)) - end - - def values_at(*indices) - indices.map { |key| self[convert_key(key)] } - end - - def merge(other) - dup.merge!(other) - end - - def merge!(other) - other.each do |key, value| - self[convert_key(key)] = value - end - self - end - - # Convert to a Hash with String keys. - def to_hash - Hash.new(default).merge!(self) - end - - protected - - def convert_key(key) - key.is_a?(Symbol) ? key.to_s : key - end - - # Magic predicates. For instance: - # - # options.force? # => !!options['force'] - # options.shebang # => "/usr/lib/local/ruby" - # options.test_framework?(:rspec) # => options[:test_framework] == :rspec - # rubocop:disable DoubleNegation - def method_missing(method, *args) - method = method.to_s - if method =~ /^(\w+)\?$/ - if args.empty? - !!self[$1] - else - self[$1] == args.first - end - else - self[method] - end - end - end - end -end diff --git a/middleman-core/middleman-core.gemspec b/middleman-core/middleman-core.gemspec index 3df76827..7fbc48d7 100644 --- a/middleman-core/middleman-core.gemspec +++ b/middleman-core/middleman-core.gemspec @@ -52,4 +52,7 @@ Gem::Specification.new do |s| # Testing s.add_dependency('contracts', ['~> 0.9.0']) + + # Immutability + s.add_dependency('hamster', ['~> 1.0']) end diff --git a/middleman-core/spec/middleman-core/util_spec.rb b/middleman-core/spec/middleman-core/util_spec.rb index 631fd856..f339a31c 100644 --- a/middleman-core/spec/middleman-core/util_spec.rb +++ b/middleman-core/spec/middleman-core/util_spec.rb @@ -54,30 +54,30 @@ describe Middleman::Util do end describe "::recursively_enhance" do - it "returns HashWithIndifferentAccess if given one" do - input = Middleman::Util::HashWithIndifferentAccess.new({test: "subject"}) + it "returns IndifferentHash if given one" do + input = Middleman::Util::IndifferentHash.new({test: "subject"}) subject = Middleman::Util.recursively_enhance input - expect( subject ).to be_a Middleman::Util::HashWithIndifferentAccess + expect( subject ).to be_a Middleman::Util::IndifferentHash expect( subject.test ).to eq "subject" end - it "returns HashWithIndifferentAccess if given a hash" do + it "returns IndifferentHash if given a hash" do input = {test: "subject"} subject = Middleman::Util.recursively_enhance input - expect( subject ).to be_a Middleman::Util::HashWithIndifferentAccess + expect( subject ).to be_a Middleman::Util::IndifferentHash expect( subject.test ).to eq "subject" end - it "returns Array with strings, or HashWithIndifferentAccess, true, false" do - indifferent_hash = Middleman::Util::HashWithIndifferentAccess.new({test: "subject"}) + it "returns Array with strings, or IndifferentHash, true, false" do + indifferent_hash = Middleman::Util::IndifferentHash.new({test: "subject"}) regular_hash = {regular: "hash"} input = [ indifferent_hash, regular_hash, true, false ] subject = Middleman::Util.recursively_enhance input - expect( subject[0] ).to be_a Middleman::Util::HashWithIndifferentAccess - expect( subject[1] ).to be_a Middleman::Util::HashWithIndifferentAccess + expect( subject[0] ).to be_a Middleman::Util::IndifferentHash + expect( subject[1] ).to be_a Middleman::Util::IndifferentHash expect( subject[1].regular ).to eq "hash" expect( subject[2] ).to eq true expect( subject[3] ).to eq false