Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Hashie::Extensions::Mash::DefineAccessors. #323

Merged
merged 1 commit into from
Jan 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Metrics/AbcSize:
# Offense count: 2
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 209
Max: 212

# Offense count: 7
Metrics/CyclomaticComplexity:
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions lib/hashie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ module Mash
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
Expand Down
90 changes: 90 additions & 0 deletions lib/hashie/extensions/mash/define_accessors.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions lib/hashie/mash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ def to_module(mash_method_name = :settings)
end
end

def with_accessors!
extend Hashie::Extensions::Mash::DefineAccessors
end

alias to_s inspect

# If you pass in an existing hash, it will
Expand Down
90 changes: 90 additions & 0 deletions spec/hashie/extensions/mash/define_accessors_spec.rb
Original file line number Diff line number Diff line change
@@ -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