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.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/README.md +92 -0
- data/Rakefile +2 -0
- data/functional_test/config.json +28 -0
- data/functional_test/test.rake +95 -0
- data/lib/tamper.rb +15 -0
- data/lib/tamper/bitmap_pack.rb +26 -0
- data/lib/tamper/existence_pack.rb +101 -0
- data/lib/tamper/integer_pack.rb +43 -0
- data/lib/tamper/pack.rb +58 -0
- data/lib/tamper/pack_set.rb +120 -0
- data/lib/tamper/version.rb +3 -0
- data/spec/bitmap_pack_spec.rb +48 -0
- data/spec/existence_pack_spec.rb +157 -0
- data/spec/integer_pack_spec.rb +85 -0
- data/spec/pack_set_spec.rb +64 -0
- data/spec/pack_spec.rb +18 -0
- data/spec/spec_helper.rb +10 -0
- data/tamper.gemspec +26 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -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
data/README.md
ADDED
@@ -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`
|
data/Rakefile
ADDED
@@ -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
|
data/lib/tamper.rb
ADDED
@@ -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
|
data/lib/tamper/pack.rb
ADDED
@@ -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,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
|
data/spec/pack_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
data/tamper.gemspec
ADDED
@@ -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
|