faster_open_struct 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|