packed-model 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rdoc", "~> 3.12"
10
+ gem "jeweler", "~> 1.8.4"
11
+ gem "rcov", ">= 0"
12
+ gem "rspec", ">= 0"
13
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,32 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.2.4)
5
+ git (1.2.5)
6
+ jeweler (1.8.4)
7
+ bundler (~> 1.0)
8
+ git (>= 1.2.5)
9
+ rake
10
+ rdoc
11
+ json (1.7.7)
12
+ rake (10.0.4)
13
+ rcov (1.0.0)
14
+ rdoc (3.12.2)
15
+ json (~> 1.4)
16
+ rspec (2.13.0)
17
+ rspec-core (~> 2.13.0)
18
+ rspec-expectations (~> 2.13.0)
19
+ rspec-mocks (~> 2.13.0)
20
+ rspec-core (2.13.1)
21
+ rspec-expectations (2.13.0)
22
+ diff-lcs (>= 1.1.3, < 2.0)
23
+ rspec-mocks (2.13.1)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ jeweler (~> 1.8.4)
30
+ rcov
31
+ rdoc (~> 3.12)
32
+ rspec
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Doug Youch
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = packed-model
2
+
3
+ Ruby library for storing model data in their binary format
4
+
5
+ == Contributing to packed-model
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
9
+ * Fork the project.
10
+ * Start a feature/bugfix branch.
11
+ * Commit and push until you are happy with your contribution.
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2013 Doug Youch. See LICENSE.txt for
18
+ further details.
19
+
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "packed-model"
18
+ gem.homepage = "http://github.com/dyouch5@yahoo.com/packed-model"
19
+ gem.license = "MIT"
20
+ gem.summary = "PackedModel stores model data in a binary string"
21
+ gem.description = "Used to minimize storage space required to store list of data"
22
+ gem.email = "doug@sessionm.com"
23
+ gem.authors = ["Doug Youch"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/test_*.rb'
39
+ test.verbose = true
40
+ test.rcov_opts << '--exclude "gems/*"'
41
+ end
42
+
43
+ task :default => :test
44
+
45
+ require 'rdoc/task'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "packed-model #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,4 @@
1
+ require 'packed_model/errors'
2
+ require 'packed_model/base'
3
+ require 'packed_model/list'
4
+
@@ -0,0 +1,247 @@
1
+ module PackedModel
2
+ class Base
3
+ def self.fields
4
+ []
5
+ end
6
+
7
+ def fields
8
+ self.class.fields
9
+ end
10
+
11
+ def self.keys
12
+ []
13
+ end
14
+
15
+ def self.all_field_options
16
+ {}
17
+ end
18
+
19
+ def self.field_options(key)
20
+ all_field_options[key.to_sym]
21
+ end
22
+
23
+ def self.field_index(key)
24
+ field_options(key)[:index]
25
+ end
26
+
27
+ def keys
28
+ self.class.keys
29
+ end
30
+
31
+ def self.fixed_width?
32
+ true
33
+ end
34
+
35
+ def fixed_width?
36
+ self.class.fixed_width?
37
+ end
38
+
39
+ def self.attribute(name, options={}, &block)
40
+ options[:name] = name.to_sym
41
+ raise "duplicate field name #{name}" if self.keys.include?(options[:name])
42
+ options[:index] = self.fields.size
43
+ options[:type] ||= :string
44
+ case options[:type]
45
+ when :string
46
+ options[:strip] = true
47
+ options[:pack_directive] = 'Z*' # null terminated string
48
+ when :integer, :int
49
+ options[:default] ||= 0 # pack 'N' directive requires a value for an integer
50
+ options[:bytesize] = 4
51
+ options[:pack_directive] = 'N' # network byte order
52
+ when :char
53
+ options[:size] ||= 1
54
+ options[:bytesize] = options[:size]
55
+ options[:pack_directive] = "a#{options[:size]}"
56
+ when :marker
57
+ raise "missing value for marker" unless options[:value]
58
+ options[:default] = options[:value]
59
+ options[:bytesize] = 4
60
+ options[:pack_directive] = 'N' # network byte order
61
+ when :custom
62
+ raise "missing pack_directive for #{name}" unless options[:pack_directive]
63
+ else
64
+ raise "Unknown type #{options[:type]}"
65
+ end
66
+
67
+ class_eval do
68
+ attrs = self.fields << options
69
+ ks = self.keys << options[:name]
70
+ defaults = self.default_values << options[:default]
71
+ ps = self.pack_string << options[:pack_directive]
72
+ kopts = self.all_field_options.merge name.to_sym => options
73
+
74
+ class << self; self end.send(:define_method, "fields") do
75
+ attrs
76
+ end
77
+ class << self; self end.send(:define_method, "keys") do
78
+ ks
79
+ end
80
+ class << self; self end.send(:define_method, "default_values") do
81
+ defaults
82
+ end
83
+ class << self; self end.send(:define_method, "pack_string") do
84
+ ps
85
+ end
86
+ class << self; self end.send(:define_method, "all_field_options") do
87
+ kopts
88
+ end
89
+
90
+ if self.fixed_width? && options[:bytesize].nil?
91
+ class << self; self end.send(:define_method, "fixed_width?") do
92
+ false
93
+ end
94
+ class << self; self end.send(:define_method, "bytesize") do
95
+ nil
96
+ end
97
+ end
98
+
99
+ if self.fixed_width?
100
+ bs = self.bytesize + options[:bytesize]
101
+ class << self; self end.send(:define_method, "bytesize") do
102
+ bs
103
+ end
104
+ end
105
+ end
106
+
107
+ method_src = <<-METHOD
108
+ def #{name}
109
+ @values[#{options[:index]}] || #{options[:default].inspect}
110
+ end
111
+ METHOD
112
+
113
+ case options[:type]
114
+ when :string, :char
115
+ method_src << <<-METHOD
116
+ def #{name}=(val)
117
+ val = val.nil? ? nil : val.to_s
118
+ @changed = @changed || @values[#{options[:index]}] != val
119
+ @values[#{options[:index]}] = val
120
+ end
121
+ METHOD
122
+ when :integer, :int
123
+ method_src << <<-METHOD
124
+ def #{name}=(val)
125
+ val = val.nil? ? nil : val.to_i
126
+ @changed = @changed || @values[#{options[:index]}] != val
127
+ @values[#{options[:index]}] = val
128
+ end
129
+ METHOD
130
+ when :custom
131
+ if options[:filter].is_a?(Symbol) || options[:filter].is_a?(String)
132
+ method_src << <<-METHOD
133
+ def #{name}=(val)
134
+ val = val.nil? ? nil : val.#{options[:filter]}
135
+ @changed = @changed || @values[#{options[:index]}] != val
136
+ @values[#{options[:index]}] = val
137
+ end
138
+ METHOD
139
+ elsif options[:filter] ||= block
140
+ method_src << <<-METHOD
141
+ def #{name}=(val)
142
+ val = val.nil? ? nil : self.class.field_options(#{name.inspect})[:filter].call(val)
143
+ @changed = @changed || @values[#{options[:index]}] != val
144
+ @values[#{options[:index]}] = val
145
+ end
146
+ METHOD
147
+ else
148
+ method_src << <<-METHOD
149
+ def #{name}=(val)
150
+ @changed = @changed || @values[#{options[:index]}] != val
151
+ @values[#{options[:index]}] = val
152
+ end
153
+ METHOD
154
+ end
155
+ end
156
+
157
+ self.class_eval method_src, __FILE__, __LINE__
158
+ end
159
+
160
+ def initialize(values=nil)
161
+ case values
162
+ when String
163
+ raise InvalidDataException.new("fixed width model expected #{self.bytesize} but got #{values.bytesize}") if self.fixed_width? && self.bytesize != values.bytesize
164
+ self.unpack values
165
+ when Hash
166
+ @changed = true
167
+ @values = []
168
+ values.each do |k, v|
169
+ send("#{k}=", v) if self.class.field_options(k)
170
+ end
171
+ # set default values
172
+ fields.each_with_index do |field, idx|
173
+ @values[idx] ||= self.class.default_values[idx]
174
+ end
175
+ when NilClass
176
+ @changed = true
177
+ @values = self.class.default_values.dup
178
+ else
179
+ raise InvalidDataException.new("invalid values")
180
+ end
181
+ end
182
+
183
+ def values
184
+ @values
185
+ end
186
+
187
+ def changed?
188
+ !! @changed
189
+ end
190
+
191
+ def changed!
192
+ @changed = true
193
+ end
194
+
195
+ def not_changed!
196
+ @changed = false
197
+ end
198
+
199
+ def to_hash
200
+ Hash[self.keys.zip @values]
201
+ end
202
+ alias to_h to_hash
203
+
204
+ def pack
205
+ @values.pack self.class.pack_string
206
+ end
207
+
208
+ def bytesize
209
+ self.class.fixed_width? ? self.class.bytesize : (@bytesize ||= self.pack.bytesize)
210
+ end
211
+ alias size bytesize
212
+
213
+ def unpack(str)
214
+ @values = str.unpack(self.class.pack_string)
215
+ self.fields.each do |field|
216
+ case field[:type]
217
+ when :string
218
+ if @values[field[:index]].empty?
219
+ @values[field[:index]] = nil
220
+ else
221
+ @values[field[:index]].strip!
222
+ end
223
+ when :char
224
+ @values[field[:index]].strip! if field[:strip]
225
+ when :marker
226
+ marker_value = @values[field[:index]]
227
+ raise BadMarkerException.new("unpack failed, invalid marker value (#{marker_value}) expected #{field[:value]}") unless marker_value == field[:value]
228
+ end
229
+ end
230
+ end
231
+
232
+ private
233
+
234
+ def self.default_values
235
+ []
236
+ end
237
+
238
+ def self.bytesize
239
+ 0
240
+ end
241
+
242
+ def self.pack_string
243
+ ""
244
+ end
245
+ end
246
+ end
247
+
@@ -0,0 +1,5 @@
1
+ module PackedModel
2
+ class BadMarkerException < Exception; end
3
+ class InvalidDataException < Exception; end
4
+ class InvalidPackedModelException < Exception; end
5
+ end
@@ -0,0 +1,126 @@
1
+ module PackedModel
2
+ class List
3
+ include Enumerable
4
+
5
+ def initialize(row_class, values=nil)
6
+ raise InvalidPackedModelException.new("row_class must be fixed width") unless row_class.fixed_width?
7
+ @row_class = row_class
8
+ @buffer = values
9
+ end
10
+
11
+ # the data in each row is only unpacked when it is accessed from here
12
+ def [](idx)
13
+ if (row = rows[idx]).is_a?(String)
14
+ row = rows[idx] = @row_class.new(row)
15
+ end
16
+ row
17
+ end
18
+
19
+ def []=(idx, row)
20
+ row.changed!
21
+ rows[idx] = row
22
+ end
23
+
24
+ def <<(row)
25
+ row.changed!
26
+ rows << row
27
+ end
28
+
29
+ def remove(idx)
30
+ @must_repack = true
31
+ rows[idx] = nil
32
+ end
33
+
34
+ def pack
35
+ return repack if @must_repack
36
+ return @buffer unless @rows
37
+ @buffer ||= ''
38
+ start = 0
39
+ @rows.each_with_index do |row, idx|
40
+ # only updated dirty rows
41
+ if ! row.is_a?(String) && row.changed?
42
+ @buffer[start...(start+row_bytesize)] = row.pack
43
+ row.not_changed!
44
+ end
45
+ start += row_bytesize
46
+ end
47
+ @buffer
48
+ end
49
+
50
+ # repacks the buffer
51
+ def repack
52
+ @buffer = ''
53
+ self.each do |row|
54
+ next unless row
55
+ @buffer << row.pack
56
+ end
57
+ remove_instance_variable "@rows"
58
+ remove_instance_variable("@must_repack") if defined?(@must_repack)
59
+ remove_instance_variable("@results") if defined?(@results)
60
+ @buffer
61
+ end
62
+
63
+ # to search the buffer the first field must be a marker and the 2nd is the what you are searching for
64
+ # Example: class TestSearchableModel < PackedModel::Base
65
+ # attribute :magic, :type => :marker, :value => 20130502
66
+ # attribute :id, :type => :integer
67
+ # attribute :name, :type => :char, :size => 20
68
+ # end
69
+ def find_in_buffer(val)
70
+ return nil unless @buffer
71
+ @results ||= {}
72
+ return @results[val] if @results.has_key?(val)
73
+ index = @buffer.index search_string(val)
74
+ return (@results[val] = nil) unless index
75
+ index = index / row_bytesize
76
+ @results[val] = self[index]
77
+ end
78
+
79
+ def size
80
+ rows.size
81
+ end
82
+
83
+ def bytesize
84
+ row_bytesize * size
85
+ end
86
+
87
+ def each
88
+ self.size.times do |idx|
89
+ yield self[idx]
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ # doesn't really unpack the data into models, just creates an array of pointers into the buffer
96
+ # data is not unpacked unless it is accessed
97
+ def unpack(str)
98
+ raise InvalidDataException.new("invalid string for list") unless (str.bytesize % row_bytesize) == 0
99
+
100
+ start = 0
101
+ [].tap do |data|
102
+ (str.bytesize / row_bytesize).times do
103
+ data << str[start...(start+row_bytesize)] # essentially we are creating C style pointers into the string at certain locations
104
+ start += row_bytesize
105
+ end
106
+ end
107
+ end
108
+
109
+ def rows
110
+ if ! defined?(@rows)
111
+ @rows = @buffer ? unpack(@buffer) : []
112
+ end
113
+ @rows
114
+ end
115
+
116
+ def row_bytesize
117
+ @row_class.bytesize
118
+ end
119
+
120
+ def search_string(val)
121
+ marker_field = @row_class.fields[0]
122
+ key_field = @row_class.fields[1]
123
+ [marker_field[:value], val].pack "#{marker_field[:pack_directive]}#{key_field[:pack_directive]}"
124
+ end
125
+ end
126
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ begin
5
+ Bundler.setup(:default, :development)
6
+ rescue Bundler::BundlerError => e
7
+ $stderr.puts e.message
8
+ $stderr.puts "Run `bundle install` to install missing gems"
9
+ exit e.status_code
10
+ end
11
+
12
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
+ require 'packed_model'
@@ -0,0 +1,200 @@
1
+ require 'helper'
2
+
3
+ describe PackedModel::Base do
4
+ context "fixed width model testing" do
5
+ class TestFixedPackedModel < PackedModel::Base
6
+ attribute :magic, :type => :marker, :value => 20130501
7
+ attribute :id, :type => :integer
8
+ attribute :name, :type => :char, :size => 20, :strip => true
9
+ attribute :blob, :type => :char, :size => 4, :default => 'MP3'
10
+ attribute :count, :type => :integer, :default => 1
11
+
12
+ def self.test_packed_string
13
+ "\0013*\305\000\000\000Ztester\000\000\000\000\000\000\000\000\000\000\000\000\000\000WAV\000\000\000\000\005"
14
+ end
15
+
16
+ def self.test_hash
17
+ {:name => "tester", :id => '90', :count => 5, :blob => 'WAV'}
18
+ end
19
+ end
20
+
21
+ it "should be a fixed width model" do
22
+ TestFixedPackedModel.fixed_width?.should be_true
23
+ end
24
+
25
+ it "should have a bytesize of 36" do
26
+ TestFixedPackedModel.bytesize.should == 36
27
+ end
28
+
29
+ it "should construct a pack string based on the order of the attributes" do
30
+ TestFixedPackedModel.pack_string.should == "NNa20a4N"
31
+ end
32
+
33
+ it "should be able to get the index for a field" do
34
+ TestFixedPackedModel.keys.should == [:magic, :id, :name, :blob, :count]
35
+ TestFixedPackedModel.field_index(:blob).should == 3
36
+ end
37
+
38
+ it "should default the count to 1" do
39
+ TestFixedPackedModel.new.tap do |m|
40
+ m.changed?.should be_true
41
+ m.id.should == 0
42
+ m.name.should be_nil
43
+ m.blob.should == "MP3"
44
+ m.count.should == 1
45
+ end
46
+ end
47
+
48
+ it "should pack the model data into a binary string" do
49
+ TestFixedPackedModel.new(TestFixedPackedModel.test_hash).tap do |m|
50
+ m.changed?.should be_true
51
+ m.id.should == 90
52
+ m.name.should == "tester"
53
+ m.blob.should == "WAV"
54
+ m.count.should == 5
55
+ m.pack.should == TestFixedPackedModel.test_packed_string
56
+ m.pack.bytesize.should == TestFixedPackedModel.bytesize
57
+ end
58
+ end
59
+
60
+ it "should not be changed if initialized from a packed string" do
61
+ TestFixedPackedModel.new(TestFixedPackedModel.test_packed_string).tap do |m|
62
+ m.changed?.should be_false
63
+ m.id.should == 90
64
+ m.name.should == "tester"
65
+ m.blob.should == "WAV\000" # expected not stripping after unpacking
66
+ m.count.should == 5
67
+ m.pack.should == TestFixedPackedModel.test_packed_string
68
+ m.pack.bytesize.should == TestFixedPackedModel.bytesize
69
+ end
70
+ end
71
+
72
+ it "should raise BadMarkerException marker values do not match" do
73
+ TestFixedPackedModel.new.tap do |m|
74
+ str = m.pack
75
+ str[0] = 'z'
76
+
77
+ expect {
78
+ TestFixedPackedModel.new str
79
+ }.to raise_exception(PackedModel::BadMarkerException)
80
+ end
81
+ end
82
+
83
+ it "should not accept strings that do not have enough data to unpack" do
84
+ expect {
85
+ TestFixedPackedModel.new 'bad data'
86
+ }.to raise_exception(PackedModel::InvalidDataException)
87
+ end
88
+
89
+ it "should be changed of a field value changes" do
90
+ TestFixedPackedModel.new(TestFixedPackedModel.test_packed_string).tap do |m|
91
+ m.changed?.should be_false
92
+ m.name = "tester"
93
+ m.changed?.should be_false
94
+ m.name = "different"
95
+ m.changed?.should be_true
96
+ m.pack.should == "\0013*\305\000\000\000Zdifferent\000\000\000\000\000\000\000\000\000\000\000WAV\000\000\000\000\005"
97
+ m.changed?.should be_true
98
+ m.not_changed!
99
+ m.changed?.should be_false
100
+ end
101
+ end
102
+
103
+ it "should be able to_hash the model data" do
104
+ TestFixedPackedModel.new(TestFixedPackedModel.test_hash).to_hash.should ==
105
+ { :magic => 20130501,
106
+ :name => 'tester',
107
+ :id => 90,
108
+ :blob => "WAV",
109
+ :count => 5
110
+ }
111
+ end
112
+ end
113
+
114
+ context "custom field model testing" do
115
+ class TestCustomPackedModel < PackedModel::Base
116
+ attribute :magic, :type => :marker, :value => 20130501
117
+ attribute :id, :type => :integer
118
+ attribute :special, :type => :custom, :pack_directive => 'n', :filter => :to_i, :bytesize => 2 # 16-bit integer
119
+ attribute :name, :type => :char, :size => 4
120
+
121
+ def self.test_packed_string
122
+ "\0013*\305\000\000\000Z\000Y\000\000\000\000"
123
+ end
124
+
125
+ def self.test_hash
126
+ {:id => '90', :special => '89'}
127
+ end
128
+ end
129
+
130
+ it "should be a fixed width model" do
131
+ TestCustomPackedModel.fixed_width?.should be_true
132
+ end
133
+
134
+ it "should have a bytesize of 14" do
135
+ TestCustomPackedModel.bytesize.should == 14
136
+ end
137
+
138
+ it "should properly transform values for the special field to integers" do
139
+ TestCustomPackedModel.new(:special => '90').tap do |m|
140
+ m.id.should == 0
141
+ m.special.should == 90
142
+ m.pack.bytesize.should == TestCustomPackedModel.bytesize
143
+ end
144
+ end
145
+
146
+ it "should be able to pack/unpack custom fields" do
147
+ m1 = TestCustomPackedModel.new TestCustomPackedModel.test_hash
148
+ m1.pack.should == TestCustomPackedModel.test_packed_string
149
+ m1.id.should == 90
150
+ m1.special.should == 89
151
+
152
+ m2 = TestCustomPackedModel.new TestCustomPackedModel.test_packed_string
153
+ m2.id.should == 90
154
+ m2.special.should == 89
155
+ end
156
+ end
157
+
158
+ context "non fixed width model testing" do
159
+ class TestNonFixedWidthPackedModel < PackedModel::Base
160
+ attribute :magic, :type => :marker, :value => 20130502
161
+ attribute :name, :type => :string
162
+ attribute :count, :type => :integer
163
+ attribute :description, :type => :string
164
+ attribute :summary, :type => :string
165
+ attribute :ratings, :type => :integer
166
+
167
+ def self.test_packed_string
168
+ "\0013*\306Mr. Foo Bar\000\000\000\000P\000Foobartastic\000\000\000\000\000"
169
+ end
170
+
171
+ def self.test_hash
172
+ {:name => "Mr. Foo Bar", :count => '80', :summary => 'Foobartastic'}
173
+ end
174
+ end
175
+
176
+ it "should not be a fixed width" do
177
+ TestNonFixedWidthPackedModel.fixed_width?.should be_false
178
+ TestNonFixedWidthPackedModel.bytesize.should be_nil
179
+ end
180
+
181
+ it "should be able to pack/unpack non fixed with models" do
182
+ m1 = TestNonFixedWidthPackedModel.new TestNonFixedWidthPackedModel.test_hash
183
+ m1.name.should == "Mr. Foo Bar"
184
+ m1.count.should == 80
185
+ m1.description.should be_nil
186
+ m1.summary.should == 'Foobartastic'
187
+ m1.ratings.should == 0
188
+ m1.pack.should == TestNonFixedWidthPackedModel.test_packed_string
189
+ m1.bytesize.should == 38
190
+
191
+ m2 = TestNonFixedWidthPackedModel.new TestNonFixedWidthPackedModel.test_packed_string
192
+ m2.name.should == "Mr. Foo Bar"
193
+ m2.count.should == 80
194
+ m2.description.should be_nil
195
+ m2.summary.should == 'Foobartastic'
196
+ m2.ratings.should == 0
197
+ m2.bytesize.should == 38
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,140 @@
1
+ require 'helper'
2
+
3
+ describe PackedModel::List do
4
+ class TestListPackedModel < PackedModel::Base
5
+ attribute :magic, :type => :marker, :value => 20130501
6
+ attribute :id, :type => :integer
7
+ attribute :name, :type => :char, :size => 20, :strip => true
8
+
9
+ def self.test_list_packed_string
10
+ "\0013*\305\000\000\000\005foo\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\0013*\305\000\000\002\274bar\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\0013*\305\000\000\000\006game\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"
11
+ end
12
+
13
+ def self.test_list_with_models
14
+ PackedModel::List.new(TestListPackedModel).tap do |list|
15
+ list << TestListPackedModel.new(:id => 5, :name => "foo")
16
+ list << TestListPackedModel.new(:id => 700, :name => "bar")
17
+ list << TestListPackedModel.new(:id => 6, :name => "game")
18
+ end
19
+ end
20
+ end
21
+
22
+ class TestInvalidListPackedModel < PackedModel::Base
23
+ attribute :magic, :type => :marker, :value => 20130501
24
+ attribute :id, :type => :integer
25
+ attribute :name, :type => :string
26
+ end
27
+
28
+ context "initialize" do
29
+ it "should only allow fixed width PackedModels" do
30
+ TestListPackedModel.fixed_width?.should be_true
31
+ PackedModel::List.new TestListPackedModel
32
+
33
+ TestInvalidListPackedModel.fixed_width?.should be_false
34
+ expect {
35
+ PackedModel::List.new TestInvalidListPackedModel
36
+ }.to raise_exception(PackedModel::InvalidPackedModelException)
37
+ end
38
+
39
+ it "should be able to pack a list of models" do
40
+ list = TestListPackedModel.test_list_with_models
41
+ list.pack.should == TestListPackedModel.test_list_packed_string
42
+ list.size.should == 3
43
+ end
44
+
45
+ it "should be able to initialize a list from a packed string" do
46
+ PackedModel::List.new(TestListPackedModel, TestListPackedModel.test_list_packed_string).tap do |list|
47
+ list.map(&:name).should == ['foo', 'bar', 'game']
48
+ list.size.should == 3
49
+ end
50
+ end
51
+
52
+ it "should not perform any operations on the buffer unless the data is accessed" do
53
+ PackedModel::List.new(TestListPackedModel, 'bad data').tap do |list|
54
+ list.pack.should == 'bad data'
55
+ expect {
56
+ list[0].name
57
+ }.to raise_exception(PackedModel::InvalidDataException)
58
+ end
59
+ end
60
+ end
61
+
62
+ context "pack/unpack" do
63
+ it "should only update the buffer if the row has changed" do
64
+ PackedModel::List.new(TestListPackedModel, TestListPackedModel.test_list_packed_string).tap do |list|
65
+ list[1].name = 'changed'
66
+ list[1].changed?.should be_true
67
+ list.pack.should_not == TestListPackedModel.test_list_packed_string
68
+ end
69
+
70
+ PackedModel::List.new(TestListPackedModel, TestListPackedModel.test_list_packed_string).tap do |list|
71
+ list[1].name = 'changed'
72
+ list[1].changed?.should be_true
73
+
74
+ # change the model state to not changed
75
+ list[1].not_changed!
76
+ list[1].changed?.should be_false
77
+
78
+ # when packing the data the list skips over unchanged rows
79
+ list.pack.should == TestListPackedModel.test_list_packed_string
80
+ end
81
+ end
82
+
83
+ it "should be able to add new rows to the list" do
84
+ PackedModel::List.new(TestListPackedModel, TestListPackedModel.test_list_packed_string).tap do |list|
85
+ list << (item = TestListPackedModel.new(:name => "new item", :id => 500))
86
+ list.pack.should == "#{TestListPackedModel.test_list_packed_string}#{item.pack}"
87
+ end
88
+
89
+ PackedModel::List.new(TestListPackedModel, TestListPackedModel.test_list_packed_string).tap do |list|
90
+ list[list.size] = (item = TestListPackedModel.new(:name => "new item", :id => 500))
91
+ list.pack.should == "#{TestListPackedModel.test_list_packed_string}#{item.pack}"
92
+ end
93
+ end
94
+ end
95
+
96
+ context "find_in_buffer" do
97
+ it "should be able to find rows in the buffer" do
98
+ PackedModel::List.new(TestListPackedModel, TestListPackedModel.test_list_packed_string).tap do |list|
99
+ list.find_in_buffer(700).tap do |item|
100
+ item.should_not be_nil
101
+ item.name.should == "bar"
102
+ end
103
+
104
+ list.find_in_buffer(999999).tap do |item|
105
+ item.should be_nil
106
+ end
107
+
108
+ # only the row found is unpacked
109
+ list.send(:rows).tap do |rows|
110
+ rows[0].is_a?(String)
111
+ rows[1].is_a?(TestListPackedModel)
112
+ rows[2].is_a?(String)
113
+ end
114
+
115
+ list.find_in_buffer(700).tap do |item|
116
+ item.should_not be_nil
117
+ item.name.should == "bar"
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ context "remove" do
124
+ it "should be able to remove a row from the list" do
125
+ PackedModel::List.new(TestListPackedModel, TestListPackedModel.test_list_packed_string).tap do |list|
126
+ item1 = list[0]
127
+ item1.name.should == "foo"
128
+ item2 = list[2]
129
+ item2.name.should == "game"
130
+ list.remove 1
131
+ list.pack.should == "#{item1.pack}#{item2.pack}"
132
+ list.map(&:name).should == ["foo", "game"]
133
+
134
+ PackedModel::List.new(TestListPackedModel, list.pack).tap do |list2|
135
+ list2.map(&:name).should == ["foo", "game"]
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
15
+ require 'packed_model'
16
+
17
+ class Test::Unit::TestCase
18
+ end
@@ -0,0 +1,12 @@
1
+ require 'helper'
2
+
3
+ class TestPackedModelBase < Test::Unit::TestCase
4
+ should "create a PackedModel::Base with different columns" do
5
+ class TestModel < PackedModel::Base
6
+ attribute :magic, :type => :marker, :value => 20130501
7
+ attribute :name, :type => :char, :size => 20
8
+ attribute :count, :type => :integer
9
+ end
10
+ TestModel.fixed_width?.should be_true
11
+ end
12
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: packed-model
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Doug Youch
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2013-05-02 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ prerelease: false
22
+ type: :development
23
+ name: rdoc
24
+ version_requirements: &id001 !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 3
30
+ - 12
31
+ version: "3.12"
32
+ requirement: *id001
33
+ - !ruby/object:Gem::Dependency
34
+ prerelease: false
35
+ type: :development
36
+ name: jeweler
37
+ version_requirements: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 1
43
+ - 8
44
+ - 4
45
+ version: 1.8.4
46
+ requirement: *id002
47
+ - !ruby/object:Gem::Dependency
48
+ prerelease: false
49
+ type: :development
50
+ name: rcov
51
+ version_requirements: &id003 !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 0
57
+ version: "0"
58
+ requirement: *id003
59
+ - !ruby/object:Gem::Dependency
60
+ prerelease: false
61
+ type: :development
62
+ name: rspec
63
+ version_requirements: &id004 !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ requirement: *id004
71
+ description: Used to minimize storage space required to store list of data
72
+ email: doug@sessionm.com
73
+ executables: []
74
+
75
+ extensions: []
76
+
77
+ extra_rdoc_files:
78
+ - LICENSE.txt
79
+ - README.rdoc
80
+ files:
81
+ - .document
82
+ - Gemfile
83
+ - Gemfile.lock
84
+ - LICENSE.txt
85
+ - README.rdoc
86
+ - Rakefile
87
+ - VERSION
88
+ - lib/packed_model.rb
89
+ - lib/packed_model/base.rb
90
+ - lib/packed_model/errors.rb
91
+ - lib/packed_model/list.rb
92
+ - spec/helper.rb
93
+ - spec/packed_model/base_spec.rb
94
+ - spec/packed_model/list_spec.rb
95
+ - test/helper.rb
96
+ - test/test_packed_model_base.rb
97
+ has_rdoc: true
98
+ homepage: http://github.com/dyouch5@yahoo.com/packed-model
99
+ licenses:
100
+ - MIT
101
+ post_install_message:
102
+ rdoc_options: []
103
+
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ segments:
111
+ - 0
112
+ version: "0"
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ segments:
118
+ - 0
119
+ version: "0"
120
+ requirements: []
121
+
122
+ rubyforge_project:
123
+ rubygems_version: 1.3.6
124
+ signing_key:
125
+ specification_version: 3
126
+ summary: PackedModel stores model data in a binary string
127
+ test_files: []
128
+