tamper 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 52b5b9d6757695b8df127daf7020a10feef37c27
4
+ data.tar.gz: 84a666eb3e032eed862a6b2deb4ff3baa315387e
5
+ SHA512:
6
+ metadata.gz: 5022114100f3fa105def51905c8964979b79d7d78557312da37dd34f7744fd306c74dba3c608fa694227ef6b037bdd89cf5c1f7a046275e4fc98b5f1c6d1e164
7
+ data.tar.gz: 258e49d4efb9473f7e3b5d5abb064e6f3ca15cb90d75e400b56e570ce0151ff6ba05cffe0c038dee0584fd1602db313f6d4e36b1061d8e410f47c8893b747736
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in tamper.gemspec
4
+ gemspec
5
+
6
+ gem 'rspec'
7
+ gem 'byebug'
8
+ gem 'json'
9
+ gem 'rake'
@@ -0,0 +1,92 @@
1
+ # Tamper Ruby encoder
2
+
3
+ To encode data from ruby:
4
+
5
+ * Create a new `Tamper::PackSet`.
6
+ * Define attributes that should be packed with `PackSet#add_attribute`
7
+ * Dump the pack as JSON with `PackSet#to_json`
8
+
9
+ ### Init
10
+
11
+ ```ruby
12
+ @pack_set = Tamper::PackSet.new
13
+ ```
14
+
15
+ ### Setting metadata on the pack
16
+
17
+ This metadata is only necessary if you're buffing attributes,
18
+ or your application requires extended info about the pack.
19
+
20
+ ```ruby
21
+ @pack_set.meta = {
22
+ buffer_url: "http://",
23
+ buffer_callback: 'getDetails'
24
+ }
25
+ ```
26
+
27
+ ### Adding a packed attribute
28
+
29
+ These are the required attributes, you can set additional
30
+ keys if required by your application:
31
+
32
+ ```ruby
33
+ @pack_set.add_attribute(
34
+ name: :bucket,
35
+ possibilities: @project.buckets.map(&:downcase),
36
+ max_choices: 1
37
+ )
38
+ ```
39
+
40
+ ### Adding a buffered attribute
41
+
42
+ ```ruby
43
+ @pack_set.add_buffered_attribute(
44
+ name: 'name'
45
+ )
46
+ ```
47
+
48
+ ### Encoding data
49
+
50
+ ** Important Note: ** the `id` attribute must be numeric. Tamper cannot pack string IDs, since ExistencePack compression relies on contiguous ranges. If the `id` is a string, it will be coerced to an int.
51
+
52
+ #### If you can provide an array of hashes upfront
53
+
54
+
55
+ ```ruby
56
+ # Keys can be either symbols or strings
57
+ data = [{ 'id' : '1', 'zip_code' : '06475' }, { 'id' : '1', 'zip_code' : '91647' }]
58
+
59
+ @pack_set.pack!(data)
60
+ ```
61
+
62
+ #### If you need to iterate (for example when doing a find_in_batches)
63
+
64
+ ```ruby
65
+ @pack_set.build_pack(max_guid: max_guid) do |pack|
66
+ photo.each do |p|
67
+ pack << {
68
+ id: p.serial,
69
+ bucket: p.submission.bucket,
70
+ zip_code: p.zip_code,
71
+ state: p.submitter.state
72
+ }
73
+ end
74
+ end
75
+ ```
76
+
77
+ ### Outputting pack
78
+
79
+ ```ruby
80
+ puts @pack_set.to_json
81
+ ```
82
+
83
+ ### For more info
84
+
85
+ See [protocol specs](https://github.com/newsdev/tamper/wiki/Packs) on the Tamper wiki.
86
+
87
+ ### Running tests
88
+
89
+ ruby-tamper ships with:
90
+
91
+ * rspecs, which can be run with `bundle exec rspec`
92
+ * functional tests to generate output based on reference datasets. Results are compared to the canonical-output in the root of the project. Run these with `bundle exec rake functional:test`
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ load "./functional_test/test.rake"
@@ -0,0 +1,28 @@
1
+ {
2
+ "attrs": [
3
+ {"attr_name": "gender",
4
+ "display_name": "Integer Pack",
5
+ "max_choices": 1,
6
+ "possibilities": ["male","female","cis"],
7
+ "filter_type": "category",
8
+ "display_type": "string"},
9
+ {"attr_name": "bmp",
10
+ "display_name": "Bitmap Attr",
11
+ "max_choices": 8,
12
+ "possibilities": ["a","b","c","d","e","f","g","h"],
13
+ "filter_type": "category",
14
+ "display_type": "string"},
15
+ {"attr_name": "unbmp",
16
+ "display_name": "Unaligned Bitmap Attr",
17
+ "max_choices": 2,
18
+ "possibilities": ["left","right","up","down"],
19
+ "filter_type": "category",
20
+ "display_type": "string"},
21
+ {"attr_name": "num",
22
+ "display_name": "Numeric Attr",
23
+ "max_choices": 2,
24
+ "possibilities": ["1","2","3","4","5","6","7","8","9","10"],
25
+ "filter_type": "category",
26
+ "display_type": "string"}
27
+ ]
28
+ }
@@ -0,0 +1,95 @@
1
+ require 'json'
2
+ require 'tamper'
3
+
4
+ # The test/datasets folder at the root of the project
5
+ # contains a variety of reference datasets.
6
+ #
7
+ # `rake functional:generate` will write generate tamper output for each
8
+ # test file and write the result to output/ruby-tamper.
9
+ #
10
+ # Use `rake functional:compare` to compare the outputs to the canonical version.
11
+ namespace :functional do
12
+
13
+ desc "Generate functional outputs, then compare them"
14
+ task :test do
15
+ Rake::Task["functional:generate"].invoke
16
+ Rake::Task["functional:compare"].invoke
17
+ end
18
+
19
+ desc "Generate functional outputs for this gem using the test inputs."
20
+ task :generate do
21
+ base_path = File.dirname(__FILE__)
22
+ config = JSON.parse(File.read(File.join(base_path, 'config.json')), symbolize_names: true)
23
+ inputs = Dir.glob(File.join(base_path, '..', '..', '..', 'test', 'datasets', '*.json'))
24
+ output_dir = File.join(base_path, 'output')
25
+
26
+ puts "Sweeping output dir..."
27
+ FileUtils.rm_f(File.join(output_dir, '*.json'))
28
+ FileUtils.mkdir_p(output_dir)
29
+
30
+ puts "Generating test output for ruby-tamper."
31
+
32
+ inputs.each do |input_file|
33
+ print "Packing #{File.basename(input_file)}... "
34
+ @pack_set = Tamper::PackSet.new
35
+ config[:attrs].each { |attr| @pack_set.add_attribute(attr) }
36
+
37
+ begin
38
+ data = JSON.parse(File.read(input_file))
39
+ @pack_set.pack!(data['items'], guid_attr: 'guid')
40
+ puts "Success!"
41
+ rescue Exception => e
42
+ puts; puts "ERROR: #{e.class} #{e.message}"
43
+ puts e.backtrace.select { |l| l.match('lib/tamper') }.join("\n")
44
+ exit(1)
45
+ end
46
+
47
+ File.open(File.join(output_dir, File.basename(input_file)), 'w') { |f| f.write(@pack_set.to_json) }
48
+ end
49
+ end
50
+
51
+ desc "Run this to diff output vs. canonical outputs."
52
+ task :compare do
53
+ base_path = File.dirname(__FILE__)
54
+ reference_dir = File.join(base_path, '..', '..', '..', 'test', 'canonical-output')
55
+ output_files = Dir.glob(File.join(base_path, 'output', '*.json'))
56
+ diffs = []
57
+
58
+ output_files.each do |test_output|
59
+ test_file = File.basename(test_output)
60
+
61
+ puts "\nResults for: #{test_file}"
62
+ reference_data = JSON.parse(File.read(File.join(reference_dir, test_file)))
63
+
64
+ ruby_data = JSON.parse(File.read(test_output))
65
+
66
+ diffs << diff('existence', reference_data['existence']['pack'], ruby_data['existence']['pack'])
67
+
68
+ reference_data['attributes'].each do |reference_attr|
69
+ ruby_attr = ruby_data['attributes'].detect { |attr| attr['attr_name'] == reference_attr['attr_name'] }
70
+ ruby_attr.delete('max_guid')
71
+ reference_attr.delete('max_guid')
72
+ diffs << diff(reference_attr['attr_name'], reference_attr, ruby_attr)
73
+ end
74
+ end
75
+
76
+ puts
77
+ if diffs.any? { |d| d == false }
78
+ puts "**** There were errors in this test run."
79
+ else
80
+ puts "Functional tests OK!"
81
+ end
82
+ end
83
+
84
+ # Returns true if values were the same, false otherwise.
85
+ def diff(attr_name, var1, var2)
86
+ if var1 == var2
87
+ puts " #{attr_name} is the same."
88
+ return true
89
+ else
90
+ puts " ERROR on #{attr_name}!\n reference was #{var1}\n but ruby was #{var2}"
91
+ return false
92
+ end
93
+ end
94
+
95
+ end
@@ -0,0 +1,15 @@
1
+ require 'bitset'
2
+ require 'base64'
3
+ require 'json'
4
+ require 'oj'
5
+
6
+ require 'tamper/version'
7
+ require 'tamper/pack_set'
8
+ require 'tamper/pack'
9
+ require 'tamper/existence_pack'
10
+ require 'tamper/integer_pack'
11
+ require 'tamper/bitmap_pack'
12
+
13
+ module Tamper
14
+ # Your code goes here...
15
+ end
@@ -0,0 +1,26 @@
1
+ module Tamper
2
+ class BitmapPack < Pack
3
+
4
+ def encoding
5
+ :bitmap
6
+ end
7
+
8
+ def encode(idx, data)
9
+ choice_data = data[attr_name.to_sym] || data[attr_name.to_s]
10
+ choice_data = [choice_data] unless choice_data.is_a?(Array)
11
+
12
+ item_offset = idx * item_window_width
13
+ choice_data.each do |choice|
14
+ choice_offset = possibilities.index(choice.to_s)
15
+ @bitset[item_offset + choice_offset] = true if choice_offset
16
+ end
17
+ end
18
+
19
+ def initialize_pack!(max_guid, num_items)
20
+ @bit_window_width = 1
21
+ @item_window_width = possibilities.length
22
+ @bitset = Bitset.new(item_window_width * num_items)
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,101 @@
1
+ module Tamper
2
+ class ExistencePack < Pack
3
+
4
+ def initialize
5
+ @output = ''
6
+ @current_chunk = ''
7
+ @last_guid = 0
8
+ @run_counter = 0
9
+ end
10
+
11
+ def initialize_pack!(max_guid, num_items)
12
+ end
13
+
14
+ def encoding
15
+ :existence
16
+ end
17
+
18
+ def encode(guid)
19
+ guid_diff = guid.to_i - @last_guid
20
+ guid_diff += 1 if @current_chunk.empty? && @output.empty? && guid.to_i > 0
21
+
22
+ if guid_diff == 1 || guid.to_i == 0 # guid is 1 step forward
23
+ @current_chunk << '1'
24
+ @run_counter += 1
25
+
26
+ elsif guid_diff <= 0 # somehow we went backwards or didn't change guid on iteration
27
+ raise ArgumentError, "Error: data was not sorted by GUID (got #{@last_guid}, then #{guid})!"
28
+
29
+ elsif guid_diff > 40 # big gap, encode with skip control char
30
+ dump_keep(@current_chunk, @run_counter)
31
+
32
+ @output += control_code(:skip, guid_diff - 1)
33
+ @current_chunk = '1'
34
+ @run_counter = 1
35
+
36
+ else # skips < 40 should just be encoded as '0'
37
+ if @run_counter > 40 # first check if a run came before this 0; if so dump it
38
+ dump_keep(@current_chunk, @run_counter)
39
+ @current_chunk = ''
40
+ @run_counter = 0
41
+ end
42
+
43
+ @current_chunk += ('0' * (guid_diff - 1))
44
+ @current_chunk << '1'
45
+ @run_counter = 1
46
+ end
47
+
48
+ @last_guid = guid.to_i
49
+ end
50
+
51
+ def finalize_pack!
52
+ dump_keep(@current_chunk, @run_counter)
53
+ raise "Encoding error, #{@output.length} is not an even number of bytes!" if @output.length % 8 > 0
54
+ @bitset = Bitset.from_s(@output)
55
+ end
56
+
57
+ def to_h
58
+ { encoding: encoding,
59
+ pack: encoded_bitset }
60
+ end
61
+
62
+
63
+ private
64
+ def dump_keep(chunk, run_len)
65
+ if run_len >= 40
66
+ dump_keep(chunk[0, chunk.length - run_len], 0)
67
+ @output += control_code(:run, run_len)
68
+ elsif !(chunk.nil? || chunk.empty?)
69
+ @output += control_code(:keep, chunk.length)
70
+ @output += chunk
71
+
72
+ # If the keep is not an even number of bytes, pad with 0 until it can be evenly packed
73
+ if (@output.length % 8) > 0
74
+ @output += '0' * (8 - (@output.length % 8))
75
+ end
76
+ end
77
+ end
78
+
79
+ def control_code(cmd, offset=0)
80
+ case cmd
81
+ when :keep
82
+ control_seq = '00000000'
83
+ bytes_to_keep = offset / 8
84
+ control_seq += bytes_to_keep.to_s(2).rjust(32)
85
+
86
+ remaining_bits = offset % 8
87
+ control_seq += remaining_bits.to_s(2).rjust(8)
88
+ when :skip
89
+ control_seq = '00000001'
90
+ control_seq += offset.to_s(2).rjust(32)
91
+ when :run
92
+ control_seq = '00000010'
93
+ control_seq += offset.to_s(2).rjust(32)
94
+ else
95
+ raise "Unknown control cmd '#{cmd}'!"
96
+ end
97
+ control_seq
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,43 @@
1
+ module Tamper
2
+ class IntegerPack < Pack
3
+
4
+ def encoding
5
+ :integer
6
+ end
7
+
8
+ def encode(idx, data)
9
+ choice_data = data[attr_name.to_sym] || data[attr_name.to_s]
10
+ choice_data = [choice_data] unless choice_data.is_a?(Array)
11
+
12
+ (0...max_choices).each do |choice_idx|
13
+ choice_offset = (item_window_width * idx) + (bit_window_width * choice_idx)
14
+
15
+ value = choice_data[choice_idx]
16
+
17
+ # TODO: test and handle nil case
18
+ if possibility_idx = possibilities.index(value.to_s)
19
+ possibility_id = possibility_idx + 1
20
+ else
21
+ possibility_id = 0
22
+ end
23
+
24
+ bit_code = possibility_id.to_i.to_s(2).split('') # converts to str binary representation
25
+ bit_code_length_pad = bit_window_width - bit_code.length
26
+ bit_code.each_with_index do |bit, bit_idx|
27
+ byebug if (choice_offset + bit_code_length_pad + bit_idx) == -1
28
+ @bitset[(choice_offset + bit_code_length_pad + bit_idx)] = bit == "1"
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ def initialize_pack!(max_guid, num_items)
35
+ @bit_window_width = Math.log2(possibilities.length + 1).ceil # add 1 to possiblities.length for the implicit nil possiblity.
36
+ @bit_window_width = 1 if @bit_window_width == 0 # edge case: 1 possibility
37
+
38
+ @item_window_width = bit_window_width * max_choices
39
+ @bitset = Bitset.new(item_window_width * num_items)
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,58 @@
1
+ module Tamper
2
+ class Pack
3
+ attr_reader :attr_name, :possibilities, :max_choices, :encoding, :bitset
4
+
5
+ attr_reader :bit_window_width, :item_window_width, :bitset
6
+
7
+ attr_reader :max_guid
8
+
9
+ attr_accessor :meta
10
+
11
+ def initialize(attr_name, possibilities, max_choices)
12
+ @attr_name, @possibilities, @max_choices = attr_name, possibilities, max_choices
13
+ @meta = {}
14
+
15
+ raise ArgumentError, "Possibilities are empty for #{attr_name}!" if possibilities.nil? || possibilities.empty?
16
+ @possibilities.map!(&:to_s) # tamper values/possibilities should always be strings.
17
+ end
18
+
19
+ def self.build(attr_name, possibilities, max_choices)
20
+ if (max_choices * Math.log2(possibilities.length)) < possibilities.length
21
+ pack = IntegerPack
22
+ else
23
+ pack = BitmapPack
24
+ end
25
+
26
+ pack.new(attr_name, possibilities, max_choices)
27
+ end
28
+
29
+ def to_h
30
+ output = { encoding: encoding,
31
+ attr_name: attr_name,
32
+ possibilities: possibilities,
33
+ pack: encoded_bitset,
34
+ item_window_width: item_window_width,
35
+ bit_window_width: bit_window_width,
36
+ max_choices: max_choices }
37
+ output.merge(meta)
38
+ end
39
+
40
+ # Most packs do not implement this.
41
+ def finalize_pack!
42
+ data = @bitset.to_s
43
+ byte_length = data.length / 8
44
+ remaining_bits = data.length % 8
45
+
46
+ output = byte_length.to_s(2).rjust(32)
47
+ output += remaining_bits.to_s(2).rjust(8)
48
+ output += data
49
+
50
+ @bitset = Bitset.from_s(output)
51
+ end
52
+
53
+ private
54
+ def encoded_bitset
55
+ Base64.strict_encode64(@bitset.marshal_dump[:data].unpack('b*').pack('B*')) if @bitset
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,120 @@
1
+ module Tamper
2
+ class PackSet
3
+
4
+ attr_accessor :meta, :existence_pack
5
+
6
+ def initialize(opts={})
7
+ @existence_pack = ExistencePack.new
8
+ @attr_packs = {}
9
+ @buffered_attrs = {}
10
+ @meta = opts
11
+ end
12
+
13
+ def add_attribute(opts)
14
+ opts = opts.dup
15
+ [:attr_name, :possibilities, :max_choices].each do |required_opt|
16
+ raise ArgumentError, ":#{required_opt} is required when adding an attribute!" if !opts.key?(required_opt)
17
+ end
18
+
19
+ name = opts.delete(:attr_name)
20
+ possibilities = opts.delete(:possibilities).compact
21
+ max_choices = opts.delete(:max_choices)
22
+
23
+ pack = Pack.build(name, possibilities, max_choices)
24
+ pack.meta = opts
25
+ @attr_packs[name.to_sym] = pack
26
+ pack
27
+ end
28
+
29
+
30
+ # Buffered attributes will not be packed, but their metadata will be included in the PackSet's JSON
31
+ # representation. Clients will expect these attrs to be available via the <tt>buffer_url</tt>.
32
+ def add_buffered_attribute(opts)
33
+ opts = opts.dup
34
+ raise ArgumentError, ":attr_name is required when adding a buffered attribute!" if !opts.key?(:attr_name)
35
+
36
+ attr_name = opts.delete(:attr_name)
37
+ @buffered_attrs[attr_name.to_sym] = { attr_name: attr_name }.merge(opts)
38
+ end
39
+
40
+ def attributes
41
+ @attr_packs.keys
42
+ end
43
+
44
+ def pack_for(attr)
45
+ @attr_packs[attr]
46
+ end
47
+
48
+ def pack!(data, opts={})
49
+ opts = opts.dup
50
+ opts[:guid_attr] ||= 'id'
51
+ opts[:max_guid] ||= (data.last[opts[:guid_attr].to_sym] || data.last[opts[:guid_attr].to_s])
52
+ opts[:num_items] ||= data.length
53
+
54
+ build_pack(opts) do |p|
55
+ data.each { |d| p << d }
56
+ end
57
+ end
58
+
59
+ def build_pack(opts={}, &block)
60
+ guid_attr = opts[:guid_attr] || 'id'
61
+ packs = @attr_packs.values
62
+ max_guid = opts[:max_guid]
63
+ num_items = opts[:num_items]
64
+
65
+ [:num_items, :max_guid].each do |required_opt|
66
+ raise ArgumentError, "You must specify :#{required_opt} to start building a pack!" if !opts.key?(required_opt)
67
+ end
68
+
69
+ existence_pack.initialize_pack!(max_guid, num_items)
70
+ packs.each { |p| p.initialize_pack!(max_guid, num_items) }
71
+
72
+ idx = 0
73
+ packer = ->(d) {
74
+ guid = d[guid_attr.to_sym] || d[guid_attr.to_s]
75
+ existence_pack.encode(guid)
76
+ packs.each { |p| p.encode(idx, d) }
77
+ idx += 1
78
+ }
79
+ packer.instance_eval { alias :<< :call; alias :add :call }
80
+
81
+ yield(packer)
82
+
83
+ existence_pack.finalize_pack!
84
+ packs.each { |p| p.finalize_pack! }
85
+ end
86
+
87
+ def build_unordered_pack(opts={}, &block)
88
+ guid_attr = opts[:guid_attr] || 'id'
89
+ data = {}
90
+
91
+ extractor = ->(d) {
92
+ guid = d[guid_attr.to_sym] || d[guid_attr.to_s]
93
+ data[guid] = d
94
+ }
95
+ extractor.instance_eval { alias :<< :call; alias :add :call }
96
+
97
+ yield(extractor)
98
+
99
+ sorted_guids = data.keys.sort
100
+ sorted_data = sorted_guids.map { |guid| data[guid] }
101
+ pack!(sorted_data, opts)
102
+ end
103
+
104
+ def to_hash(opts={})
105
+ output = {
106
+ version: '2.1',
107
+ existence: @existence_pack.to_h,
108
+ attributes: @attr_packs.values.map { |p| p.to_h }
109
+ }
110
+
111
+ output[:attributes] += @buffered_attrs.values
112
+ output.merge!(meta)
113
+ output
114
+ end
115
+
116
+ def to_json(opts={})
117
+ Oj.dump self, { mode: :compat }
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,3 @@
1
+ module Tamper
2
+ VERSION = "0.2.2"
3
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tamper::BitmapPack do
4
+
5
+ before do
6
+ @data = [
7
+ { 'id' => 0, 'color' => ['yellow','blue'] },
8
+ { 'id' => 1, 'color' => ['blue', 'yellow'] },
9
+ { 'id' => 2, 'color' => ['red'] },
10
+ ]
11
+
12
+ @pack_set = Tamper::PackSet.new
13
+ @pack_set.add_attribute(attr_name: :color,
14
+ possibilities: ['yellow', 'red', 'blue', 'purple'],
15
+ max_choices: 2)
16
+ @pack_set.pack!(@data)
17
+
18
+ @color_pack = @pack_set.pack_for(:color)
19
+ @color_pack.should be_a(Tamper::BitmapPack)
20
+ end
21
+
22
+ its "item_window_width is equal to the number of possibilities, since multiple bits can be flipped for each item" do
23
+ @color_pack.item_window_width.should == ['yellow','red','blue','purple'].length
24
+ end
25
+
26
+ its "length is equal to the number of choices * the number of items" do
27
+ @color_pack.bitset.size.should == (4 * 3) + 32 + 8
28
+ end
29
+
30
+ it "encodes the length at the front of the pack" do
31
+ @color_pack.bitset.to_s[0,32].to_i(2).should == 1
32
+ @color_pack.bitset.to_s[32,8].to_i(2).should == 4
33
+ end
34
+
35
+ it "flips a bit true for each selected choice" do
36
+ str_bits = @color_pack.bitset.to_s
37
+
38
+ # first item should have idx 0 set to true since it's yellow, idx 2 since it's blue
39
+ str_bits[40,4].should == '1010'
40
+
41
+ # second item should have the same, order that possibilities are in does not matter
42
+ str_bits[44,4].should == '1010'
43
+
44
+ # last item should have pos 2 set to true since it's red
45
+ str_bits[48,4].should == '0100'
46
+ end
47
+
48
+ end
@@ -0,0 +1,157 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tamper::ExistencePack do
4
+
5
+ describe "with continguous ids" do
6
+ before do
7
+ @data = [
8
+ { 'id' => 0 },
9
+ { 'id' => 1 },
10
+ { 'id' => 2 }
11
+ ]
12
+
13
+ @pack_set = Tamper::PackSet.new
14
+ @pack_set.pack!(@data)
15
+
16
+ @existence_pack = @pack_set.existence_pack
17
+ @existence_pack.should be_a(Tamper::ExistencePack)
18
+ @bits = @existence_pack.bitset.to_s
19
+ end
20
+
21
+ it "sets a control code at the start to keep all ids" do
22
+ @bits[0,8].should == '00000000'
23
+ end
24
+
25
+ it "includes 0 bytes in existence bitmap" do
26
+ @bits[8,32].to_i(2).should == 0
27
+ end
28
+
29
+ it "includes 3 remainder bits in existence bitmap" do
30
+ @bits[40,8].to_i(2).should == 3
31
+ end
32
+
33
+ it "sets all ids as existing, then pads with zeroes" do
34
+ @bits[48,8].should == '11100000'
35
+ end
36
+ end
37
+
38
+ describe "with gaps in ids" do
39
+ before do
40
+ @data = [
41
+ { 'id' => 0 },
42
+ { 'id' => 1 },
43
+ { 'id' => 2 },
44
+ { 'id' => 6 },
45
+ { 'id' => 60 },
46
+ { 'id' => 61 },
47
+ { 'id' => 62 },
48
+ ]
49
+
50
+ @pack_set = Tamper::PackSet.new
51
+ @pack_set.pack!(@data)
52
+
53
+ @existence_pack = @pack_set.existence_pack
54
+ @existence_pack.should be_a(Tamper::ExistencePack)
55
+ @bits = @existence_pack.bitset.to_s
56
+ end
57
+
58
+ it "keeps all ids with gaps < 20 in a contiguous set" do
59
+ @bits[0,8].should == '00000000' # keep code
60
+ @bits[8,32].to_i(2).should == 0 # keep 0 bytes of data
61
+ @bits[40,8].to_i(2).should == 7 # keep 7 remainder bits
62
+ @bits[48,8].to_s.should == '11100010' # encode 1-6
63
+ end
64
+
65
+ it "sets a skip control char if the gap is > 40" do
66
+ @bits[56,8].should == '00000001'
67
+ @bits[64,32].to_i(2).should == 53
68
+ end
69
+
70
+ it "sets a keep control char after the gap" do
71
+ @bits[96,8].should == '00000000' # keep
72
+ @bits[104,32].to_i(2).should == 0 # keep 0 bytes
73
+ @bits[136,8].to_i(2).should == 3 # keep 3 remainder bits
74
+ end
75
+
76
+ it "sets bits correctly after a skipped gap" do
77
+ @bits[144,8].to_s.should == '11100000'
78
+ end
79
+ end
80
+
81
+ describe "with a run of ids" do
82
+ before do
83
+ guids = [(0..59).to_a, 67,68].flatten
84
+ @data = guids.map { |g| { 'id' => g } }
85
+ @pack_set = Tamper::PackSet.new
86
+ @pack_set.pack!(@data)
87
+
88
+ @existence_pack = @pack_set.existence_pack
89
+ @existence_pack.should be_a(Tamper::ExistencePack)
90
+ @bits = @existence_pack.bitset.to_s
91
+ end
92
+
93
+ it "sets a run for the continguous ids" do
94
+ @bits[0,8].should == '00000010' # run code
95
+ @bits[8,32].to_i(2).should == 60 # run of 60 guids
96
+ end
97
+
98
+ it "sets a keep control char after the run" do
99
+ @bits[40,8].should == '00000000' # keep
100
+ @bits[48,32].to_i(2).should == 1 # keep 0 bytes
101
+ @bits[80,8].to_i(2).should == 1 # keep 1 remaining bit
102
+ end
103
+
104
+ it "sets bits correctly after a skipped gap" do
105
+ @bits[88,16].to_s.should == '00000001' + '10000000'
106
+ end
107
+ end
108
+
109
+ describe "with a guid sequence that starts with a skip" do
110
+ before do
111
+ @data = [
112
+ { 'id' => 100 },
113
+ { 'id' => 101 },
114
+ { 'id' => 102 }
115
+ ]
116
+
117
+ @pack_set = Tamper::PackSet.new
118
+ @pack_set.pack!(@data)
119
+
120
+ @existence_pack = @pack_set.existence_pack
121
+ @existence_pack.should be_a(Tamper::ExistencePack)
122
+ @bits = @existence_pack.bitset.to_s
123
+ end
124
+
125
+ it "sets a skip control code at the start of the set" do
126
+ @bits[0,8].should == '00000001' # skip
127
+ @bits[8,32].to_i(2).should == 100 # skip 99 guids
128
+ end
129
+ end
130
+
131
+ describe "the example from the README" do
132
+ before do
133
+
134
+ test_guids = [0,1,2,3,6,10,12,14,16,17,18,19,20,21,22,23,25,31,32,97]
135
+ @data = test_guids.map { |guid| { 'id' => guid }}
136
+
137
+ @pack_set = Tamper::PackSet.new
138
+ @pack_set.pack!(@data)
139
+
140
+ @existence_pack = @pack_set.existence_pack
141
+ @existence_pack.should be_a(Tamper::ExistencePack)
142
+ @bits = @existence_pack.bitset.to_s
143
+ end
144
+
145
+ it "works" do
146
+ example_data = <<EOS
147
+ 00000000 00000000 00000000 00000000 00000100 00000001
148
+ 11110010 00101010 11111111 01000001 10000000 00000001
149
+ 00000000 00000000 00000000 01000000 00000000 00000000
150
+ 00000000 00000000 00000000 00000001 10000000
151
+ EOS
152
+ example_data.gsub!(/[ \n]/, '')
153
+ @bits.should == example_data
154
+ end
155
+ end
156
+
157
+ end
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tamper::IntegerPack do
4
+
5
+ describe "with a large number of possibilities" do
6
+ before do
7
+ @data = [
8
+ { 'id' => 0, 'category_id' => '14' },
9
+ { 'id' => 1, 'category_id' => '21' },
10
+ { 'id' => 2, 'category_id' => '46' },
11
+ ]
12
+
13
+ @possibilities = (0...50).to_a.map(&:to_s)
14
+
15
+ @pack_set = Tamper::PackSet.new
16
+ @pack_set.add_attribute(attr_name: :category_id,
17
+ possibilities: @possibilities,
18
+ max_choices: 1)
19
+ @pack_set.pack!(@data)
20
+
21
+ @category_pack = @pack_set.pack_for(:category_id)
22
+ @category_pack.should be_a(Tamper::IntegerPack)
23
+ end
24
+
25
+ its "bit_window_width is equal to the number of bits required to represent the max possibility id as an int, + 1 for the implicit nil poss." do
26
+ @category_pack.bit_window_width.should == @possibilities.length.to_s(2).length
27
+ end
28
+
29
+ its "item_window_width is to the bit_window_width" do
30
+ @category_pack.item_window_width.should == @category_pack.bit_window_width
31
+ end
32
+
33
+ its "overall length is equal to the item_window_width * number of items" do
34
+ @category_pack.bitset.size.should == @category_pack.item_window_width * 3 + 32 + 8
35
+ end
36
+
37
+ it "encodes the length at the front of the pack" do
38
+ @category_pack.bitset.to_s[0,32].to_i(2).should == 2
39
+ @category_pack.bitset.to_s[32,8].to_i(2).should == 2
40
+ end
41
+
42
+
43
+ it "encodes the choice id in binary for each item" do
44
+ str_bits = @category_pack.bitset.to_s
45
+
46
+ # first item should be packed to represent 14
47
+ # note that this is +1 since possibility 0 represents 'no choice'
48
+ str_bits[40,6].should == 15.to_s(2).rjust(6, '0')
49
+
50
+ # next 21
51
+ str_bits[46,6].should == 22.to_s(2).rjust(6, '0')
52
+
53
+ # last item should be packed to represent 46
54
+ str_bits[52,6].should == 47.to_s(2).rjust(6, '0')
55
+ end
56
+
57
+ end
58
+
59
+ describe "with only two possibilities" do
60
+ before do
61
+ @data = [
62
+ { 'id' => 0, 'gender' => 'female' },
63
+ { 'id' => 1, 'gender' => 'male' }
64
+ ]
65
+
66
+ @possibilities = %W(female male)
67
+
68
+ @pack_set = Tamper::PackSet.new
69
+ @pack_set.add_attribute(attr_name: :gender,
70
+ possibilities: @possibilities,
71
+ max_choices: 1)
72
+ @pack_set.pack!(@data)
73
+
74
+ @category_pack = @pack_set.pack_for(:gender)
75
+ @category_pack.should be_a(Tamper::IntegerPack)
76
+ end
77
+
78
+ its "bit_window_width is equal to the number of bits required to represent the max possibility id, + 1 for the implicit nil poss." do
79
+ @category_pack.bit_window_width.should == 2
80
+ end
81
+ end
82
+
83
+
84
+
85
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tamper::PackSet do
4
+ before do
5
+ @data = [
6
+ { 'id' => 0, 'category_id' => '14' },
7
+ { 'id' => 1, 'category_id' => '21' },
8
+ { 'id' => 2, 'category_id' => '46' },
9
+ ]
10
+
11
+ @possibilities = (0...50).to_a.map(&:to_s)
12
+
13
+ @pack_set = Tamper::PackSet.new(default_thumb: 'thumb_url')
14
+ @pack_set.add_attribute(attr_name: :category_id,
15
+ possibilities: @possibilities,
16
+ max_choices: 1,
17
+ filter_type: 'category')
18
+ end
19
+
20
+ it "packs data using a block" do
21
+ @pack_set.build_pack(max_guid: 2, num_items: @data.length) do |p|
22
+ @data.each { |d| p << d }
23
+ end
24
+
25
+ bits = @pack_set.existence_pack.bitset.to_s
26
+ bits[48,8].to_s.should == '11100000' # encode 1-3
27
+ end
28
+
29
+ it "saves additional attributes as pack.meta" do
30
+ @pack_set.pack_for(:category_id).meta.should == { filter_type: 'category' }
31
+ end
32
+
33
+ it "includes metadata passed at init as a top-level attribute in JSON" do
34
+ @pack_set.to_hash[:default_thumb].should == 'thumb_url'
35
+ end
36
+
37
+ it "jsonifies using Oj" do
38
+ @pack_set.to_json.is_a?(String).should be_true
39
+ end
40
+
41
+ it "can pack unordered data using a block" do
42
+ @data.first['id'] = 4
43
+
44
+ @pack_set.build_unordered_pack(max_guid: 4) do |p|
45
+ @data.each { |d| p << d }
46
+ end
47
+
48
+ bits = @pack_set.existence_pack.bitset.to_s
49
+ bits[48,8].to_s.should == '01101000' # encode 1-3
50
+ end
51
+
52
+ it "includes buffered attrs as part of the JSON attributes array" do
53
+ @pack_set.add_buffered_attribute(
54
+ attr_name: 'caption',
55
+ display_name: 'Display Caption',
56
+ filter_type: 'none'
57
+ )
58
+
59
+ caption_attr = @pack_set.to_hash[:attributes].detect { |attr| attr[:attr_name] == 'caption' }
60
+ caption_attr.should_not be_nil
61
+ caption_attr[:display_name].should == 'Display Caption'
62
+ end
63
+
64
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ describe Tamper::Pack do
3
+
4
+ describe ".build" do
5
+
6
+ # Some sample situations...
7
+ describe "with a large number of possibilities" do
8
+ it "creates an IntegerPack when only a single choice is allowed" do
9
+ Tamper::Pack.build(:category_id, (0...50).to_a, 1).should be_a(Tamper::IntegerPack)
10
+ end
11
+
12
+ it "creates a BitmapPack when there are a large number of choices allowed" do
13
+ Tamper::Pack.build(:category_id, (0...50).to_a, 50).should be_a(Tamper::BitmapPack)
14
+ end
15
+ end
16
+ end
17
+
18
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'bitset'
5
+ require 'tamper'
6
+ require 'byebug'
7
+
8
+ RSpec.configure do |config|
9
+ # some (optional) config here
10
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "tamper/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "tamper"
7
+ s.version = Tamper::VERSION
8
+ s.authors = ["Ben Koski"]
9
+ s.email = ["bkoski@nytimes.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Serialize defined-option attrs into miminalist binary arrays}
12
+ s.description = %q{Serialize defined-option attrs into miminalist binary arrays}
13
+
14
+ s.rubyforge_project = "tamper"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency 'bitset'
22
+ s.add_dependency 'oj'
23
+ # specify any dependencies here; for example:
24
+ # s.add_development_dependency "rspec"
25
+ # s.add_runtime_dependency "rest-client"
26
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tamper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - Ben Koski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bitset
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: oj
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Serialize defined-option attrs into miminalist binary arrays
42
+ email:
43
+ - bkoski@nytimes.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - Gemfile
49
+ - README.md
50
+ - Rakefile
51
+ - functional_test/config.json
52
+ - functional_test/test.rake
53
+ - lib/tamper.rb
54
+ - lib/tamper/bitmap_pack.rb
55
+ - lib/tamper/existence_pack.rb
56
+ - lib/tamper/integer_pack.rb
57
+ - lib/tamper/pack.rb
58
+ - lib/tamper/pack_set.rb
59
+ - lib/tamper/version.rb
60
+ - spec/bitmap_pack_spec.rb
61
+ - spec/existence_pack_spec.rb
62
+ - spec/integer_pack_spec.rb
63
+ - spec/pack_set_spec.rb
64
+ - spec/pack_spec.rb
65
+ - spec/spec_helper.rb
66
+ - tamper.gemspec
67
+ homepage: ''
68
+ licenses: []
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project: tamper
86
+ rubygems_version: 2.0.3
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Serialize defined-option attrs into miminalist binary arrays
90
+ test_files:
91
+ - spec/bitmap_pack_spec.rb
92
+ - spec/existence_pack_spec.rb
93
+ - spec/integer_pack_spec.rb
94
+ - spec/pack_set_spec.rb
95
+ - spec/pack_spec.rb
96
+ - spec/spec_helper.rb