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 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
+