tamper 0.2.2

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.
@@ -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