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 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
@@ -0,0 +1,5 @@
1
+ require "yard"
2
+ YARD.parse(File.expand_path("../faster_open_struct.rb", __FILE__))
3
+ File.open(File.expand_path("../../README.md", __FILE__), "w") do |f|
4
+ f.write(P("Faster::OpenStruct").docstring)
5
+ 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
+