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:
parent
cd30488f9e
commit
250f174f48
|
@ -13,7 +13,7 @@ Metrics/AbcSize:
|
|||
# Offense count: 2
|
||||
# Configuration parameters: CountComments.
|
||||
Metrics/ClassLength:
|
||||
Max: 209
|
||||
Max: 212
|
||||
|
||||
# Offense count: 7
|
||||
Metrics/CyclomaticComplexity:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
25
README.md
25
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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue