Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #499 from michaelherold/permissive-respond-to
Add a PermissiveRespondTo extension for Mashes
- Loading branch information
Showing
7 changed files
with
176 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
#!/usr/bin/env ruby | ||
|
||
$LOAD_PATH.unshift File.expand_path(File.join('..', 'lib'), __dir__) | ||
|
||
require 'hashie' | ||
require 'benchmark/ips' | ||
require 'benchmark/memory' | ||
|
||
permissive = Class.new(Hashie::Mash) | ||
|
||
Benchmark.memory do |x| | ||
x.report('Default') {} | ||
x.report('Make permissive') do | ||
permissive.include Hashie::Extensions::Mash::PermissiveRespondTo | ||
end | ||
end | ||
|
||
class PermissiveMash < Hashie::Mash | ||
include Hashie::Extensions::Mash::PermissiveRespondTo | ||
end | ||
|
||
Benchmark.ips do |x| | ||
x.report('Mash.new') { Hashie::Mash.new(a: 1) } | ||
x.report('Permissive.new') { PermissiveMash.new(a: 1) } | ||
|
||
x.compare! | ||
end | ||
|
||
Benchmark.ips do |x| | ||
x.report('Mash#attr=') { Hashie::Mash.new.a = 1 } | ||
x.report('Permissive#attr=') { PermissiveMash.new.a = 1 } | ||
|
||
x.compare! | ||
end | ||
|
||
mash = Hashie::Mash.new(a: 1) | ||
permissive = PermissiveMash.new(a: 1) | ||
|
||
Benchmark.ips do |x| | ||
x.report('Mash#attr= x2') { mash.a = 1 } | ||
x.report('Permissive#attr= x2') { permissive.a = 1 } | ||
|
||
x.compare! | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
module Hashie | ||
module Extensions | ||
module Mash | ||
# Allow a Mash to properly respond to everything | ||
# | ||
# By default, Mashes only say they respond to methods for keys that exist | ||
# in their key set or any of the affix methods (e.g. setter, underbang, | ||
# etc.). This causes issues when you try to use them within a | ||
# SimpleDelegator or bind to a method for a key that is unset. | ||
# | ||
# This extension allows a Mash to properly respond to `respond_to?` and | ||
# `method` for keys that have not yet been set. This enables full | ||
# compatibility with SimpleDelegator and thunk-oriented programming. | ||
# | ||
# There is a trade-off with this extension: it will run slower than a | ||
# regular Mash; insertions and initializations with keys run approximately | ||
# 20% slower and cost approximately 19KB of memory per class that you | ||
# make permissive. | ||
# | ||
# @api public | ||
# @example Make a new, permissively responding Mash subclass | ||
# class PermissiveMash < Hashie::Mash | ||
# include Hashie::Extensions::Mash::PermissiveRespondTo | ||
# end | ||
# | ||
# mash = PermissiveMash.new(a: 1) | ||
# mash.respond_to? :b #=> true | ||
module PermissiveRespondTo | ||
# The Ruby hook for behavior when including the module | ||
# | ||
# @api private | ||
# @private | ||
# @return void | ||
def self.included(base) | ||
base.instance_variable_set :@_method_cache, base.instance_methods | ||
base.define_singleton_method(:method_cache) { @_method_cache } | ||
end | ||
|
||
# The Ruby hook for determining what messages a class might respond to | ||
# | ||
# @api private | ||
# @private | ||
def respond_to_missing?(_method_name, _include_private = false) | ||
true | ||
end | ||
|
||
private | ||
|
||
# Override the Mash logging behavior to account for permissiveness | ||
# | ||
# @api private | ||
# @private | ||
def log_collision?(method_key) | ||
self.class.method_cache.include?(method_key) && | ||
!self.class.disable_warnings?(method_key) && | ||
!(regular_key?(method_key) || regular_key?(method_key.to_s)) | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
require 'spec_helper' | ||
|
||
RSpec.describe Hashie::Extensions::Mash::PermissiveRespondTo do | ||
class PermissiveMash < Hashie::Mash | ||
include Hashie::Extensions::Mash::PermissiveRespondTo | ||
end | ||
|
||
it 'allows you to bind to unset getters' do | ||
mash = PermissiveMash.new(a: 1) | ||
other_mash = PermissiveMash.new(b: 2) | ||
|
||
expect { mash.method(:b) }.not_to raise_error | ||
expect(mash.method(:b).unbind.bind(other_mash).call).to eq 2 | ||
end | ||
|
||
it 'works properly with SimpleDelegator' do | ||
delegator = Class.new(SimpleDelegator) do | ||
def initialize(hash) | ||
super(PermissiveMash.new(hash)) | ||
end | ||
end | ||
|
||
foo = delegator.new(a: 1) | ||
|
||
expect(foo.a).to eq 1 | ||
expect { foo.b }.not_to raise_error | ||
end | ||
|
||
context 'warnings' do | ||
include_context 'with a logger' | ||
|
||
it 'does not log a collision when setting normal keys' do | ||
PermissiveMash.new(a: 1) | ||
|
||
expect(logger_output).to be_empty | ||
end | ||
|
||
it 'logs a collision with a built-in method' do | ||
PermissiveMash.new(zip: 1) | ||
|
||
expect(logger_output).to match('PermissiveMash#zip defined in Enumerable') | ||
end | ||
end | ||
end |