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.
This commit is contained in:
Vladimir Kochnev 2015-11-15 16:57:51 +03:00
parent cd30488f9e
commit 250f174f48
7 changed files with 212 additions and 2 deletions

View File

@ -13,7 +13,7 @@ Metrics/AbcSize:
# Offense count: 2
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 209
Max: 212
# Offense count: 7
Metrics/CyclomaticComplexity:

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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