summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVladimir Kochnev <hashtable@yandex.ru>2015-11-15 16:57:51 +0300
committerVladimir Kochnev <hashtable@yandex.ru>2019-01-28 21:14:08 +0300
commit250f174f48a7115c832690fd052dcaf63b6debc9 (patch)
tree84c55562cdd1281072bcd84114d11859a7f5e528
parentcd30488f9e4f4cbdfe0ccada40297e6f6d7e5610 (diff)
downloadhashie-250f174f48a7115c832690fd052dcaf63b6debc9.tar.gz
Add Hashie::Extensions::Mash::DefineAccessors.
This patch adds an extension for Mash that makes it behave like `OpenStruct`. It reduces overhead of `method_missing?` magic which is a good thing! It's inspired by the recent @sferik's work on `OpenStruct` — https://github.com/ruby/ruby/pull/1033. When using it in `Mash` subclasses it makes them *remember* methods so then it's more like `ActiveModel` than `OpenStruct` in this case. To use it like `OpenStruct` one could use this shortcut: ```ruby { foo: 1, bar: 2 }.to_mash.with_accessors! ``` Implementation details: It injects to class an anonymous module that stores accessor method definitions. This is inspired by `ActiveModel` / `ActiveRecord`. It allows to override accessors in subclass and call them via `super` if this is intended.
-rw-r--r--.rubocop_todo.yml2
-rw-r--r--CHANGELOG.md2
-rw-r--r--README.md25
-rw-r--r--lib/hashie.rb1
-rw-r--r--lib/hashie/extensions/mash/define_accessors.rb90
-rw-r--r--lib/hashie/mash.rb4
-rw-r--r--spec/hashie/extensions/mash/define_accessors_spec.rb90
7 files changed, 212 insertions, 2 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 88df1a9..944b8dc 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -13,7 +13,7 @@ Metrics/AbcSize:
# Offense count: 2
# Configuration parameters: CountComments.
Metrics/ClassLength:
- Max: 209
+ Max: 212
# Offense count: 7
Metrics/CyclomaticComplexity:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b25e660..2d123ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,7 @@ scheme are considered to be bugs.
### Added
-* Your contribution here.
+* [#323](https://github.com/intridea/hashie/pull/323): Added `Hashie::Extensions::Mash::DefineAccessors` - [@marshall-lee](https://github.com/marshall-lee).
### Changed
diff --git a/README.md b/README.md
index 6a88955..77456ac 100644
--- a/README.md
+++ b/README.md
@@ -644,6 +644,31 @@ end
However, on Rubies less than 2.0, this means that every key you send to the Mash will generate a symbol. Since symbols are not garbage-collected on older versions of Ruby, this can cause a slow memory leak when using a symbolized Mash with data generated from user input.
+### Mash Extension:: DefineAccessors
+
+This extension can be mixed into a Mash so it makes it behave like `OpenStruct`. It reduces the overhead of `method_missing?` magic by lazily defining field accessors when they're requested.
+
+```ruby
+class MyHash < ::Hashie::Mash
+ include Hashie::Extensions::Mash::DefineAccessors
+end
+
+mash = MyHash.new
+MyHash.method_defined?(:foo=) #=> false
+mash.foo = 123
+MyHash.method_defined?(:foo=) #=> true
+
+MyHash.method_defined?(:foo) #=> false
+mash.foo #=> 123
+MyHash.method_defined?(:foo) #=> true
+```
+
+You can also extend the existing mash without defining a class:
+
+```ruby
+mash = ::Hashie::Mash.new.with_accessors!
+```
+
## Dash
Dash is an extended Hash that has a discrete set of defined properties and only those properties may be set on the hash. Additionally, you can set defaults for each property. You can also flag a property as required. Required properties will raise an exception if unset. Another option is message for required properties, which allow you to add custom messages for required property.
diff --git a/lib/hashie.rb b/lib/hashie.rb
index b6e1d13..fd93e05 100644
--- a/lib/hashie.rb
+++ b/lib/hashie.rb
@@ -47,6 +47,7 @@ module Hashie
autoload :KeepOriginalKeys, 'hashie/extensions/mash/keep_original_keys'
autoload :SafeAssignment, 'hashie/extensions/mash/safe_assignment'
autoload :SymbolizeKeys, 'hashie/extensions/mash/symbolize_keys'
+ autoload :DefineAccessors, 'hashie/extensions/mash/define_accessors'
end
module Array
diff --git a/lib/hashie/extensions/mash/define_accessors.rb b/lib/hashie/extensions/mash/define_accessors.rb
new file mode 100644
index 0000000..f13ea2e
--- /dev/null
+++ b/lib/hashie/extensions/mash/define_accessors.rb
@@ -0,0 +1,90 @@
+module Hashie
+ module Extensions
+ module Mash
+ module DefineAccessors
+ def self.included(klass)
+ klass.class_eval do
+ mod = Ext.new
+ include mod
+ end
+ end
+
+ def self.extended(obj)
+ included(obj.singleton_class)
+ end
+
+ class Ext < Module
+ def initialize
+ mod = self
+ define_method(:method_missing) do |method_name, *args, &block|
+ key, suffix = method_name_and_suffix(method_name)
+ case suffix
+ when '='.freeze
+ mod.define_writer(key, method_name)
+ when '?'.freeze
+ mod.define_predicate(key, method_name)
+ when '!'.freeze
+ mod.define_initializing_reader(key, method_name)
+ when '_'.freeze
+ mod.define_underbang_reader(key, method_name)
+ else
+ mod.define_reader(key, method_name)
+ end
+ send(method_name, *args, &block)
+ end
+ end
+
+ def define_reader(key, method_name)
+ define_method(method_name) do |&block|
+ if key? method_name
+ self.[](method_name, &block)
+ else
+ self.[](key, &block)
+ end
+ end
+ end
+
+ def define_writer(key, method_name)
+ define_method(method_name) do |value = nil|
+ if key? method_name
+ self.[](method_name, &proc)
+ else
+ assign_property(key, value)
+ end
+ end
+ end
+
+ def define_predicate(key, method_name)
+ define_method(method_name) do
+ if key? method_name
+ self.[](method_name, &proc)
+ else
+ !!self[key]
+ end
+ end
+ end
+
+ def define_initializing_reader(key, method_name)
+ define_method(method_name) do
+ if key? method_name
+ self.[](method_name, &proc)
+ else
+ initializing_reader(key)
+ end
+ end
+ end
+
+ def define_underbang_reader(key, method_name)
+ define_method(method_name) do
+ if key? method_name
+ self.[](key, &proc)
+ else
+ underbang_reader(key)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/hashie/mash.rb b/lib/hashie/mash.rb
index 6e45c09..f41d745 100644
--- a/lib/hashie/mash.rb
+++ b/lib/hashie/mash.rb
@@ -119,6 +119,10 @@ module Hashie
end
end
+ def with_accessors!
+ extend Hashie::Extensions::Mash::DefineAccessors
+ end
+
alias to_s inspect
# If you pass in an existing hash, it will
diff --git a/spec/hashie/extensions/mash/define_accessors_spec.rb b/spec/hashie/extensions/mash/define_accessors_spec.rb
new file mode 100644
index 0000000..de7abe7
--- /dev/null
+++ b/spec/hashie/extensions/mash/define_accessors_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Hashie::Extensions::Mash::DefineAccessors do
+ let(:args) { [] }
+
+ shared_examples 'class with dynamically defined accessors' do
+ it 'defines reader on demand' do
+ expect(subject.method_defined?(:foo)).to be_falsey
+ instance.foo
+ expect(subject.method_defined?(:foo)).to be_truthy
+ end
+
+ it 'defines writer on demand' do
+ expect(subject.method_defined?(:foo=)).to be_falsey
+ instance.foo = :bar
+ expect(subject.method_defined?(:foo=)).to be_truthy
+ end
+
+ it 'defines predicate on demand' do
+ expect(subject.method_defined?(:foo?)).to be_falsey
+ instance.foo?
+ expect(subject.method_defined?(:foo?)).to be_truthy
+ end
+
+ it 'defines initializing reader on demand' do
+ expect(subject.method_defined?(:foo!)).to be_falsey
+ instance.foo!
+ expect(subject.method_defined?(:foo!)).to be_truthy
+ end
+
+ it 'defines underbang reader on demand' do
+ expect(subject.method_defined?(:foo_)).to be_falsey
+ instance.foo_
+ expect(subject.method_defined?(:foo_)).to be_truthy
+ end
+
+ context 'when initializing from another hash' do
+ let(:args) { [{ foo: :bar }] }
+
+ it 'does not define any accessors' do
+ expect(subject.method_defined?(:foo)).to be_falsey
+ expect(subject.method_defined?(:foo=)).to be_falsey
+ expect(subject.method_defined?(:foo?)).to be_falsey
+ expect(subject.method_defined?(:foo!)).to be_falsey
+ expect(subject.method_defined?(:foo_)).to be_falsey
+ expect(instance.foo).to eq :bar
+ end
+ end
+ end
+
+ context 'when included in Mash subclass' do
+ subject { Class.new(Hashie::Mash) { include Hashie::Extensions::Mash::DefineAccessors } }
+ let(:instance) { subject.new(*args) }
+
+ describe 'this subclass' do
+ it_behaves_like 'class with dynamically defined accessors'
+
+ describe 'when accessors are overrided in class' do
+ before do
+ subject.class_eval do
+ def foo
+ if self[:foo] != 1
+ :bar
+ else
+ super
+ end
+ end
+ end
+ end
+
+ it 'allows to call super' do
+ expect(instance.foo).to eq :bar
+ instance.foo = 2
+ expect(instance.foo).to eq :bar
+ instance.foo = 1
+ expect(instance.foo).to eq 1
+ end
+ end
+ end
+ end
+
+ context 'when Mash instance is extended' do
+ let(:instance) { Hashie::Mash.new(*args).with_accessors! }
+ subject { instance.singleton_class }
+
+ describe 'its singleton class' do
+ it_behaves_like 'class with dynamically defined accessors'
+ end
+ end
+end