tamper 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|