faster_open_struct 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +15 -0
- data/lib/faster_open_struct.rb +116 -0
- data/lib/faster_open_struct_spec.rb +160 -0
- data/lib/gist_yard_readme.rb +5 -0
- metadata +70 -0
data/README.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
## Faster::OpenStruct
|
2
|
+
|
3
|
+
Up to 40 (!) times more memory efficient version of OpenStruct
|
4
|
+
|
5
|
+
Differences from Ruby MRI OpenStruct:
|
6
|
+
|
7
|
+
1. Doesn't `dup` passed initialization hash (NOTE: only reference to hash is stored)
|
8
|
+
|
9
|
+
2. Doesn't convert hash keys to symbols (by default string keys are used,
|
10
|
+
with fallback to symbol keys)
|
11
|
+
|
12
|
+
3. Creates methods on the fly on `OpenStruct` class, instead of singleton class.
|
13
|
+
Uses `module_eval` with string to avoid holding scope references for every method.
|
14
|
+
|
15
|
+
4. Refactored, crud clean, spec covered :)
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Faster
|
2
|
+
# ## Faster::OpenStruct
|
3
|
+
#
|
4
|
+
# Up to 40 (!) times more memory efficient version of OpenStruct
|
5
|
+
#
|
6
|
+
# Differences from Ruby MRI OpenStruct:
|
7
|
+
#
|
8
|
+
# 1. Doesn't `dup` passed initialization hash (NOTE: only reference to hash is stored)
|
9
|
+
#
|
10
|
+
# 2. Doesn't convert hash keys to symbols (by default string keys are used,
|
11
|
+
# with fallback to symbol keys)
|
12
|
+
#
|
13
|
+
# 3. Creates methods on the fly on `OpenStruct` class, instead of singleton class.
|
14
|
+
# Uses `module_eval` with string to avoid holding scope references for every method.
|
15
|
+
#
|
16
|
+
# 4. Refactored, crud clean, spec covered :)
|
17
|
+
#
|
18
|
+
class OpenStruct
|
19
|
+
# Undefine particularly nasty interfering methods on Ruby 1.8
|
20
|
+
undef :type if method_defined?(:type)
|
21
|
+
undef :id if method_defined?(:id)
|
22
|
+
|
23
|
+
def initialize(hash = nil)
|
24
|
+
@hash = hash || {}
|
25
|
+
@initialized_empty = hash == nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def method_missing(method_name_sym, *args)
|
29
|
+
if method_name_sym.to_s[-1] == ?=
|
30
|
+
if args.size != 1
|
31
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 1)", caller(1)
|
32
|
+
end
|
33
|
+
|
34
|
+
if self.frozen?
|
35
|
+
raise TypeError, "can't modify frozen #{self.class}", caller(1)
|
36
|
+
end
|
37
|
+
|
38
|
+
__new_ostruct_member__(method_name_sym.to_s.chomp("="))
|
39
|
+
send(method_name_sym, args[0])
|
40
|
+
elsif args.size == 0
|
41
|
+
__new_ostruct_member__(method_name_sym)
|
42
|
+
send(method_name_sym)
|
43
|
+
else
|
44
|
+
raise NoMethodError, "undefined method `#{method_name_sym}' for #{self}", caller(1)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def __new_ostruct_member__(method_name_sym)
|
49
|
+
self.class.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
|
50
|
+
def #{ method_name_sym }
|
51
|
+
@hash.fetch("#{ method_name_sym }", @hash[:#{ method_name_sym }]) # read by default from string key, then try symbol
|
52
|
+
# if string key doesn't exist
|
53
|
+
end
|
54
|
+
END_EVAL
|
55
|
+
|
56
|
+
unless method_name_sym.to_s[-1] == ?? # can't define writer for predicate method
|
57
|
+
self.class.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
|
58
|
+
def #{ method_name_sym }=(val)
|
59
|
+
if @hash.key?("#{ method_name_sym }") || @initialized_empty # write by default to string key (when it is present
|
60
|
+
# in initialization hash or initialization hash
|
61
|
+
# wasn't provided)
|
62
|
+
@hash["#{ method_name_sym }"] = val # if it doesn't exist - write to symbol key
|
63
|
+
else
|
64
|
+
@hash[:#{ method_name_sym }] = val
|
65
|
+
end
|
66
|
+
end
|
67
|
+
END_EVAL
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def empty?
|
72
|
+
@hash.empty?
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# Compare this object and +other+ for equality.
|
77
|
+
#
|
78
|
+
def ==(other)
|
79
|
+
return false unless other.is_a?(self.class)
|
80
|
+
@hash == other.instance_variable_get(:@hash)
|
81
|
+
end
|
82
|
+
|
83
|
+
InspectKey = :__inspect_key__ # :nodoc:
|
84
|
+
|
85
|
+
#
|
86
|
+
# Returns a string containing a detailed summary of the keys and values.
|
87
|
+
#
|
88
|
+
def inspect
|
89
|
+
str = "#<#{ self.class }"
|
90
|
+
str << " #{ @hash.map { |k, v| "#{ k }=#{ v.inspect }" }.join(", ") }" unless @hash.empty?
|
91
|
+
str << ">"
|
92
|
+
end
|
93
|
+
|
94
|
+
def inspect_with_reentrant_guard(default = "...")
|
95
|
+
Thread.current[InspectKey] ||= []
|
96
|
+
|
97
|
+
if Thread.current[InspectKey].include?(self)
|
98
|
+
return default # reenter detected
|
99
|
+
end
|
100
|
+
|
101
|
+
Thread.current[InspectKey] << self
|
102
|
+
|
103
|
+
begin
|
104
|
+
inspect_without_reentrant_guard
|
105
|
+
ensure
|
106
|
+
Thread.current[InspectKey].pop
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
alias_method :inspect_without_reentrant_guard, :inspect
|
111
|
+
alias_method :inspect, :inspect_with_reentrant_guard
|
112
|
+
|
113
|
+
alias :to_s :inspect
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'rspec/core'
|
2
|
+
require 'rspec/expectations'
|
3
|
+
require 'rspec/matchers'
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
share_examples_for "OpenStruct-like object" do
|
7
|
+
describe "initialization from Hash" do
|
8
|
+
describe "with symbol keys" do
|
9
|
+
it "creates reader method" do
|
10
|
+
@klass.new(:a => 42).a.should == 42
|
11
|
+
end
|
12
|
+
|
13
|
+
it "creates writer method" do
|
14
|
+
@klass.new(:a => 42).tap { |fos| fos.a = "hi!" }.a.should == "hi!"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "with string keys" do
|
19
|
+
it "creates reader method" do
|
20
|
+
@klass.new("a" => 42).a.should == 42
|
21
|
+
end
|
22
|
+
|
23
|
+
it "creates writer method" do
|
24
|
+
@klass.new(:b => 1).tap { |fos| fos.a = "hi!" }.a.should == "hi!"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "creates predicate method" do
|
28
|
+
@klass.new(:a? => 1).a?.should == 1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "initialization with writers" do
|
34
|
+
it "creates reader method when writer is called" do
|
35
|
+
@klass.new.tap { |fos| fos.a = 42 }.a.should == 42
|
36
|
+
end
|
37
|
+
|
38
|
+
it "creates writer method when writer is called" do
|
39
|
+
@klass.new.tap { |fos| fos.a = 42; fos.a = "hi!" }.a.should == "hi!"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "comparison" do
|
44
|
+
it "should compare like underlying initialization hashes" do
|
45
|
+
@klass.new(:a => 42).should == @klass.new(:a => 42)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should compare like underlying created hashes" do
|
49
|
+
@klass.new.tap { |fos| fos.a = 42 }.should == @klass.new.tap { |fos| fos.a = 42 }
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should compare like underlying initialization altered hashes" do
|
53
|
+
@klass.new(:a => 42).tap { |fos| fos.b = "hi!" }.should == @klass.new(:a => 42).tap { |fos| fos.b = "hi!" }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "error reporting" do
|
58
|
+
it "should report when too much writer arguments are supplied" do
|
59
|
+
lambda { @klass.new.send(:a=, 1, 2) }.should raise_error(ArgumentError, /wrong number of arguments .2 for 1./)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should report when too little writer arguments are supplied" do
|
63
|
+
lambda { @klass.new.send(:a=) }.should raise_error(ArgumentError, /wrong number of arguments .0 for 1./)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should report when non-writer method is called with arguments" do
|
67
|
+
lambda { @klass.new.a(1) }.should raise_error(NoMethodError, /undefined method `a'/)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should raise exception when modifying frozen" do
|
71
|
+
lambda { @klass.new.freeze.a = 1 }.should raise_error(TypeError, /can't modify frozen/)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "marshaling" do
|
76
|
+
it "should survive loading from marshaled state" do
|
77
|
+
os = @klass.new(:a => 42).tap { |fos| fos.b = "hi!" }
|
78
|
+
os.a
|
79
|
+
survivor = Marshal.load(Marshal.dump(os))
|
80
|
+
survivor.a.should == 42
|
81
|
+
survivor.b.should == "hi!"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "#inspect" do
|
86
|
+
it "works" do
|
87
|
+
@klass.new(:a => 42).tap { |fos| fos.b = "hi!" }.inspect.should match(%r{^#<.*OpenStruct a=42, b="hi!">$})
|
88
|
+
end
|
89
|
+
|
90
|
+
it "handles self-recursive cases" do
|
91
|
+
os = @klass.new
|
92
|
+
os.a = os
|
93
|
+
os.inspect.should match(%r{^#<(Faster::|)OpenStruct a=#<(Faster::|)OpenStruct \.\.\.>>$}x)
|
94
|
+
end
|
95
|
+
|
96
|
+
it "handles deep self-recursive cases" do
|
97
|
+
os = @klass.new
|
98
|
+
os.a = @klass.new
|
99
|
+
os.a.a = os
|
100
|
+
os.inspect.should match(%r{^#<(Faster::|)OpenStruct a=#<(Faster::|)OpenStruct \.\.\.>>\z}x)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "Faster::OpenStruct" do
|
106
|
+
before(:each) do
|
107
|
+
Faster.send(:remove_const, :OpenStruct) if defined?(Faster::OpenStruct)
|
108
|
+
load "./faster_open_struct.rb"
|
109
|
+
@klass = Faster::OpenStruct
|
110
|
+
|
111
|
+
if Faster::OpenStruct.method_defined?(:a) ||
|
112
|
+
Faster::OpenStruct.method_defined?(:b) ||
|
113
|
+
Faster::OpenStruct.method_defined?(:a?)
|
114
|
+
raise "reloading hack failed, clean test state is not guaranteed"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it_should_behave_like "OpenStruct-like object"
|
119
|
+
|
120
|
+
it "reponds to empty? to work seamlessly with ActiveSupport" do
|
121
|
+
@klass.new.empty?.should == true
|
122
|
+
@klass.new(:a => 1).empty?.should == false
|
123
|
+
@klass.new.tap { |os| os.a = 1 }.empty?.should == false
|
124
|
+
end
|
125
|
+
|
126
|
+
it "undefines commonly interfering methods" do
|
127
|
+
@klass.new.type == nil
|
128
|
+
@klass.new.id == nil
|
129
|
+
end
|
130
|
+
|
131
|
+
if GC.respond_to?(:enable_stats) && !ENV["SKIP_PERFORMANCE"]
|
132
|
+
describe "performance gains" do
|
133
|
+
def allocated_by_block(allocations = 10_000)
|
134
|
+
GC.clear_stats
|
135
|
+
GC.disable_stats
|
136
|
+
GC.start
|
137
|
+
GC.enable_stats
|
138
|
+
before = GC.allocated_size
|
139
|
+
eater = []
|
140
|
+
allocations.times { eater << yield }
|
141
|
+
GC.allocated_size - before
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should take 40 times less memory compared to OpenStruct" do
|
145
|
+
open_struct = allocated_by_block { OpenStruct.new({ :a => 1 }) }
|
146
|
+
faster_open_struct = allocated_by_block { Faster::OpenStruct.new({ :a => 1 }) }
|
147
|
+
(open_struct.to_f / faster_open_struct).should > 40
|
148
|
+
end
|
149
|
+
end
|
150
|
+
else
|
151
|
+
it "Use REE to test permormance gains"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
describe OpenStruct do
|
156
|
+
before(:each) do
|
157
|
+
@klass = OpenStruct
|
158
|
+
end
|
159
|
+
it_should_behave_like "OpenStruct-like object"
|
160
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: faster_open_struct
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 9
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: "0.1"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Evgeniy Dolzhenko
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-03-11 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Up to 40 (!) times more memory efficient version of OpenStruct
|
22
|
+
email:
|
23
|
+
- dolzenko@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- README.md
|
32
|
+
- lib/faster_open_struct.rb
|
33
|
+
- lib/faster_open_struct_spec.rb
|
34
|
+
- lib/gist_yard_readme.rb
|
35
|
+
has_rdoc: true
|
36
|
+
homepage: https://github.com/dolzenko/faster_open_struct
|
37
|
+
licenses: []
|
38
|
+
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
none: false
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
hash: 3
|
50
|
+
segments:
|
51
|
+
- 0
|
52
|
+
version: "0"
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
hash: 3
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
version: "0"
|
62
|
+
requirements: []
|
63
|
+
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 1.3.7
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: Up to 40 (!) times more memory efficient version of OpenStruct
|
69
|
+
test_files: []
|
70
|
+
|