pyper_rb 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/Gemfile +24 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +178 -0
  6. data/Rakefile +10 -0
  7. data/lib/pyper/all.rb +4 -0
  8. data/lib/pyper/pipeline.rb +63 -0
  9. data/lib/pyper/pipes/cassandra/all_items_reader.rb +40 -0
  10. data/lib/pyper/pipes/cassandra/deleter.rb +19 -0
  11. data/lib/pyper/pipes/cassandra/mod_key.rb +32 -0
  12. data/lib/pyper/pipes/cassandra/mod_key_reader.rb +41 -0
  13. data/lib/pyper/pipes/cassandra/pagination_decoding.rb +22 -0
  14. data/lib/pyper/pipes/cassandra/pagination_encoding.rb +17 -0
  15. data/lib/pyper/pipes/cassandra/reader.rb +35 -0
  16. data/lib/pyper/pipes/cassandra/writer.rb +24 -0
  17. data/lib/pyper/pipes/cassandra.rb +8 -0
  18. data/lib/pyper/pipes/content/fetch.rb +30 -0
  19. data/lib/pyper/pipes/content/store.rb +36 -0
  20. data/lib/pyper/pipes/content.rb +2 -0
  21. data/lib/pyper/pipes/default_values.rb +15 -0
  22. data/lib/pyper/pipes/field_rename.rb +23 -0
  23. data/lib/pyper/pipes/force_enumerator.rb +13 -0
  24. data/lib/pyper/pipes/model/attribute_deserializer.rb +27 -0
  25. data/lib/pyper/pipes/model/attribute_serializer.rb +34 -0
  26. data/lib/pyper/pipes/model/attribute_validation.rb +57 -0
  27. data/lib/pyper/pipes/model/virtus_deserializer.rb +39 -0
  28. data/lib/pyper/pipes/model/virtus_parser.rb +13 -0
  29. data/lib/pyper/pipes/model.rb +5 -0
  30. data/lib/pyper/pipes/no_op.rb +15 -0
  31. data/lib/pyper/pipes/pry.rb +9 -0
  32. data/lib/pyper/pipes/remove_fields.rb +22 -0
  33. data/lib/pyper/pipes.rb +8 -0
  34. data/lib/pyper/version.rb +3 -0
  35. data/lib/pyper.rb +4 -0
  36. data/pyper_rb.gemspec +22 -0
  37. data/test/fixtures/cass_schema_config.yml +6 -0
  38. data/test/fixtures/test_datastore/schema.cql +23 -0
  39. data/test/test_helper.rb +34 -0
  40. data/test/unit/pyper/pipeline_test.rb +81 -0
  41. data/test/unit/pyper/pipes/cassandra/all_items_reader_test.rb +47 -0
  42. data/test/unit/pyper/pipes/cassandra/deleter_test.rb +37 -0
  43. data/test/unit/pyper/pipes/cassandra/mod_key_reader_test.rb +47 -0
  44. data/test/unit/pyper/pipes/cassandra/pagination_decoding_test.rb +29 -0
  45. data/test/unit/pyper/pipes/cassandra/pagination_encoding_test.rb +29 -0
  46. data/test/unit/pyper/pipes/cassandra/reader_test.rb +79 -0
  47. data/test/unit/pyper/pipes/cassandra/writer_test.rb +51 -0
  48. data/test/unit/pyper/pipes/content/fetch_test.rb +38 -0
  49. data/test/unit/pyper/pipes/content/store_test.rb +49 -0
  50. data/test/unit/pyper/pipes/field_rename_test.rb +24 -0
  51. data/test/unit/pyper/pipes/model/attribute_deserializer_test.rb +69 -0
  52. data/test/unit/pyper/pipes/model/attribute_serializer_test.rb +60 -0
  53. data/test/unit/pyper/pipes/model/attribute_validation_test.rb +96 -0
  54. data/test/unit/pyper/pipes/model/virtus_deserializer_test.rb +75 -0
  55. data/test/unit/pyper/pipes/no_op_test.rb +12 -0
  56. data/test/unit/pyper/pipes/remove_fields_test.rb +24 -0
  57. metadata +147 -0
@@ -0,0 +1,23 @@
1
+ module Pyper::Pipes
2
+ # @param attr_map [Hash] A map of old field names to new field names, which will be used to rename attributes.
3
+ class FieldRename < Struct.new(:attr_map)
4
+
5
+ # @param args [Hash|Enumerator<Hash>] One or more item hashes
6
+ # @param status [Hash] The mutable status field
7
+ # @return [Hash|Enumerator<Hash>] The item(s) with fields renamed
8
+ def pipe(attrs_or_items, status = {})
9
+ case attrs_or_items
10
+ when Hash then rename(attrs_or_items)
11
+ else attrs_or_items.map { |item| rename(item) }
12
+ end
13
+ end
14
+
15
+ def rename(item)
16
+ attr_map.each do |old,new|
17
+ item[new.to_sym] = item.delete(old.to_sym) if item.has_key?(old.to_sym)
18
+ item[new.to_s] = item.delete(old.to_s) if item.has_key?(old.to_s)
19
+ end
20
+ item
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ module Pyper::Pipes::Model
2
+ # Typically at the end of a pipeline, makes sure any lazy computations on the items are evaluated.
3
+ # Returning a lazy enumerator can be unexpected by the consumer, and may cause the enumerator to
4
+ # be evaluated more than once with unexpected results.
5
+ class ForceEnumerator
6
+ # @param items [Enumerable::Lazy<Hash>] A list of items
7
+ # @param status [Hash] The mutable status field
8
+ # @return [Enumerable<Hash>] A list of items, deserialized according to the type mapping
9
+ def self.pipe(items, status = {})
10
+ items.force
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ require 'json'
2
+
3
+ module Pyper::Pipes::Model
4
+ # @param type_mapping [Hash<Symbol, Class>] A map from field names to types. fields will be deserialized according
5
+ # to these types.
6
+ class AttributeDeserializer < Struct.new(:type_mapping)
7
+ # @param items [Enumerable<Hash>] A list of items
8
+ # @param status [Hash] The mutable status field
9
+ # @return [Enumerable<Hash>] A list of items, deserialized according to the type mapping
10
+ def pipe(items, status = {})
11
+ items.map do |item|
12
+ new_item = item.dup
13
+ type_mapping.each do |field, type|
14
+ new_item[field] = deserialize(new_item[field], type) if new_item[field]
15
+ end
16
+ new_item
17
+ end
18
+ end
19
+
20
+ def deserialize(value, type)
21
+ if (type == Array) || (type == Hash) then JSON.parse(value)
22
+ elsif (type == Integer) then value.to_i
23
+ elsif (type == Float) then value.to_f
24
+ else value end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ require 'json'
2
+
3
+ module Pyper::Pipes::Model
4
+ # Provides a way to serialize attributes to JSON.
5
+ class AttributeSerializer
6
+
7
+ # @param attributes [Hash] Unserialized attributes
8
+ # @param status [Hash] The mutable status field
9
+ # @return [Hash] The serialized attributes
10
+ def pipe(attributes, status = {})
11
+ attributes.each_with_object({}) do |attr, serialized_attrs|
12
+ value = force_encode_to_UTF8(attr.last)
13
+ serialized_attrs[attr.first] = case value
14
+ when Array, Hash then JSON.generate(value)
15
+ when DateTime then value.to_time
16
+ else value
17
+ end
18
+ end
19
+ end
20
+
21
+ def force_encode_to_UTF8(value)
22
+ case value
23
+ when Array
24
+ value.map { |v| force_encode_to_UTF8(v) }
25
+ when Hash
26
+ Hash[value.map { |k,v| [k, force_encode_to_UTF8(v)] }]
27
+ when String
28
+ value.dup.force_encoding('UTF-8')
29
+ else
30
+ value
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,57 @@
1
+ module Pyper::Pipes::Model
2
+ class AttributeValidation
3
+
4
+ # Raised when validation fails.
5
+ class Failure < ::StandardError; end
6
+
7
+ # Array of attributes that are allowed to be set. If empty, all attributes
8
+ # are allowed.
9
+ attr_reader :allowed
10
+
11
+ # Array of attributes that are required to be present (non-nil).
12
+ attr_reader :required
13
+
14
+ # Hash of attributes whose value must be restricted in some way.
15
+ # Format :attribute => lambda { |value| #Return boolean indicating pass/fail }
16
+ attr_reader :restricted
17
+
18
+ # @param opts [Hash] Options defining how attributes should be validated.
19
+ # @option opts [Array<Symbol>] :allowed A list of attributes that are allowed
20
+ # to be set. If empty, all attributes are assumed to be allowed.
21
+ # @option opts [Array<Symbol>] :required A list of attributes that are required
22
+ # to be present (non-nil).
23
+ # @option opts [Hash] :restricted A Hash of attributes whose value must be
24
+ # restricted in some way.
25
+ # Format :attribute => lambda { |value| #Return boolean indicating pass/fail }
26
+ def initialize(opts={})
27
+ @allowed = opts[:allowed] if opts[:allowed]
28
+ @required = opts[:required] if opts[:required]
29
+ @restricted = opts[:restricted]
30
+ end
31
+
32
+ # @param attributes [Hash] The un-validated attributes
33
+ # @param status [Hash] The mutable status field
34
+ # @return [Hash] The original attributes
35
+ def pipe(attributes, status = {})
36
+ if allowed.present?
37
+ attributes.keys.each do |attr|
38
+ raise Failure.new("Attribute #{attr} is not allowed.") unless allowed.include?(attr)
39
+ end
40
+ end
41
+
42
+ if required.present?
43
+ required.each do |attr|
44
+ raise Failure.new("Missing required attribute #{attr}.") if attributes[attr].nil?
45
+ end
46
+ end
47
+
48
+ if restricted.present?
49
+ restricted.each do |attr, test|
50
+ raise Failure.new("Invalid value for attribute #{attr}.") unless test.call(attributes[attr])
51
+ end
52
+ end
53
+
54
+ attributes
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,39 @@
1
+ require 'json'
2
+
3
+ module Pyper::Pipes::Model
4
+ # Provides a way to deserialize serialized fields from an item. This is intended to be used with a Virtus
5
+ # model class, and will use the attribute names and type information from that model to determine how to
6
+ # deserialize.
7
+ #
8
+ # All serialization is as JSON.
9
+ class VirtusDeserializer
10
+
11
+ attr_reader :type_mapping
12
+
13
+ # @param attribute_set [Virtus::AttributeSet] A Virtus AttributeSet
14
+ def initialize(attribute_set)
15
+ @type_mapping = Hash[attribute_set.map { |attr| [attr.name.to_s, attr.type.primitive] }]
16
+ end
17
+
18
+ # @param items [Enumerator<Hash>] A list of items
19
+ # @param status [Hash] The mutable status field
20
+ # @return [Enumerator<Hash>] A list of items, deserialized according to the type mapping
21
+ def pipe(items, status = {})
22
+ items.map do |item|
23
+ new_item = item.dup
24
+ type_mapping.each do |field, type|
25
+ new_item[field] = deserialize(new_item[field], type) if new_item[field]
26
+ end
27
+
28
+ new_item
29
+ end
30
+ end
31
+
32
+ def deserialize(value, type)
33
+ if type == Array || type == Hash then JSON.parse(value)
34
+ elsif type == Integer then value.to_i
35
+ elsif type == Float then value.to_f
36
+ else value end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ module Pyper::Pipes::Model
2
+ # Transform a series of items into model classes (based on Virtus model objects)
3
+ # @param virtus_model_class [Class] the model class to instantiate. Should respond to `new(item_attributes)`
4
+ class VirtusParser < Struct.new(:virtus_model_class)
5
+
6
+ # @param items [Enumerable<Hash>]
7
+ # @param status [Hash] The mutable status field
8
+ # @return [Enumerable<Hash>] The unchanged list of items
9
+ def pipe(items, status = {})
10
+ items.map { |item| virtus_model_class.new(item) }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'model/attribute_serializer'
2
+ require_relative 'model/attribute_validation'
3
+ require_relative 'model/attribute_deserializer'
4
+ require_relative 'model/virtus_deserializer'
5
+ require_relative 'model/virtus_parser'
@@ -0,0 +1,15 @@
1
+ module Pyper::Pipes
2
+ # This pipe performs no operation. Usful if want to potentially skip a pipe in
3
+ # in the pipeline.
4
+ # @example
5
+ # (some condition) ? some_pipe(conent) : Pyper::Pipes::NoOp
6
+ class NoOp
7
+
8
+ # @param attrs_or_items [Object]
9
+ # @param status [Hash] The mutable status field
10
+ # @return [Object]
11
+ def self.pipe(attrs_or_items, status = {})
12
+ attrs_or_items
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ module Pyper::Pipes
2
+ # For debugging purposes
3
+ class Pry
4
+ def pipe(items, status)
5
+ binding.pry
6
+ items
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ module Pyper::Pipes
2
+ # A generic pipe to remove fields from a pipeline
3
+ class RemoveFields
4
+
5
+ attr_reader :fields_to_remove
6
+
7
+ # @param fields_to_remove [Array] fields to be removed from pipe
8
+ def initialize(fields_to_remove)
9
+ @fields_to_remove = Array.wrap(fields_to_remove)
10
+ end
11
+
12
+ # @param attributes [Hash] The attributes from which to remove the specified fields
13
+ # @param status [Hash] The mutable status field
14
+ # @return [Hash] attributes with the specified fields removed
15
+ def pipe(attributes, status = {})
16
+ attributes = attributes.dup
17
+ fields_to_remove.each { |field| attributes.delete(field) }
18
+
19
+ attributes
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,8 @@
1
+ module Pyper::Pipes; end
2
+
3
+ # require the individual generic pipes
4
+ require_relative 'pipes/field_rename'
5
+ require_relative 'pipes/default_values'
6
+ require_relative 'pipes/no_op'
7
+ require_relative 'pipes/remove_fields'
8
+ require_relative 'pipes/force_enumerator'
@@ -0,0 +1,3 @@
1
+ module Pyper
2
+ VERSION = "1.2.0"
3
+ end
data/lib/pyper.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "pyper/version"
2
+ require "pyper/pipeline"
3
+
4
+ module Pyper; end
data/pyper_rb.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pyper/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pyper_rb"
8
+ spec.version = Pyper::VERSION
9
+ spec.authors = ["Arron Norwell"]
10
+ spec.email = ["anorwell@datto.com"]
11
+ spec.summary = %q{Create pipelines for storing data.}
12
+ spec.homepage = ""
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.7"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+ end
@@ -0,0 +1,6 @@
1
+ datastores:
2
+ test_datastore:
3
+ hosts: 127.0.0.1
4
+ port: 9242
5
+ keyspace: test_keyspace
6
+ replication: "{ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }"
@@ -0,0 +1,23 @@
1
+ # CREATE KEYSPACE test_keyspace with replication = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }
2
+
3
+ CREATE TABLE test(
4
+ id text,
5
+ a text,
6
+ b text,
7
+ PRIMARY KEY((id), a)
8
+ );
9
+
10
+ CREATE TABLE test2(
11
+ id text,
12
+ x text,
13
+ y text,
14
+ PRIMARY KEY((id), x)
15
+ );
16
+
17
+ CREATE TABLE mod_test(
18
+ row int,
19
+ id text,
20
+ mod_key int,
21
+ val text,
22
+ PRIMARY KEY ((row, mod_key), id)
23
+ );
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'pry'
3
+ require 'minitest/autorun'
4
+ require 'minitest/should'
5
+
6
+ class Minitest::Should::TestCase
7
+ def self.xshould(*args)
8
+ puts "Disabled test: #{args}"
9
+ end
10
+ end
11
+
12
+ def setup_cass_schema
13
+ base_path = "#{File.dirname(__FILE__)}/fixtures"
14
+ ::CassSchema::Runner.datastores = ::CassSchema::YamlHelper.datastores(File.join(base_path, 'cass_schema_config.yml'))
15
+ ::CassSchema::Runner.schema_base_path = base_path
16
+ ::CassSchema::Runner.create_all
17
+ end
18
+
19
+ def teardown_cass_schema
20
+ ::CassSchema::Runner.drop_all
21
+ end
22
+
23
+ def create_cass_client(datastore_name)
24
+ keyspace = ::CassSchema::Runner.datastore_lookup(datastore_name).keyspace
25
+ c = Cassandra.cluster(port: 9242)
26
+ session = c.connect(keyspace)
27
+ Cassava::Client.new(session)
28
+ end
29
+
30
+
31
+ require 'pyper/all'
32
+ require 'storage_strategy'
33
+ require 'cass_schema'
34
+ require 'cassava'
@@ -0,0 +1,81 @@
1
+ require 'test_helper'
2
+
3
+ module Pyper
4
+ class PipelineTest < Minitest::Should::TestCase
5
+ context '::create' do
6
+ context 'with a block' do
7
+ should 'yield the instance to the block' do
8
+ el = 'hello'
9
+
10
+ pl = Pyper::Pipeline.create do
11
+ add el
12
+ end
13
+
14
+ assert pl.pipes.include? el
15
+ end
16
+
17
+ should 'allow access to externally defined methods' do
18
+ def external
19
+ 'external'
20
+ end
21
+
22
+ pl = Pyper::Pipeline.create do
23
+ add external
24
+ end
25
+
26
+ assert pl.pipes.include? external
27
+ end
28
+
29
+ should 'return a new pipeline' do
30
+ pl = Pyper::Pipeline.create do
31
+ add 'hello'
32
+ end
33
+
34
+ assert pl.is_a? Pyper::Pipeline
35
+ end
36
+ end
37
+
38
+ context 'without a block' do
39
+ should 'return a new pipeline' do
40
+ pl = Pyper::Pipeline.create
41
+
42
+ assert pl.is_a? Pyper::Pipeline
43
+ end
44
+ end
45
+ end
46
+
47
+ context '#add' do
48
+ should 'add the pipe to the pipes' do
49
+ pl = Pyper::Pipeline.new
50
+ el = 'hello'
51
+ pl.add el
52
+
53
+ assert pl.pipes.include? el
54
+ end
55
+ end
56
+
57
+ context '#push' do
58
+ should 'accept pipes that respond to #pipe' do
59
+ pl = Pyper::Pipeline.create do
60
+ add ExamplePipe
61
+ end
62
+ res = pl.push({})
63
+ assert res.status[:called]
64
+ end
65
+
66
+ should 'accept pipes that respond to #call' do
67
+ pl = Pyper::Pipeline.create do
68
+ add ->(attrs, status = {}) { status[:called] = true }
69
+ end
70
+ res = pl.push({})
71
+ assert res.status[:called]
72
+ end
73
+ end
74
+
75
+ class ExamplePipe
76
+ def self.pipe(attrs, status = {})
77
+ status[:called] = true
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,47 @@
1
+ require 'test_helper'
2
+
3
+ module Pyper::Pipes::Cassandra
4
+ class AllItemsReaderTest < Minitest::Should::TestCase
5
+ context 'a cassandra paging_reader pipe' do
6
+ setup do
7
+ setup_cass_schema
8
+
9
+ @client = create_cass_client('test_datastore')
10
+ @pipe = AllItemsReader.new(:test, @client)
11
+
12
+ # populate some test data
13
+ @client.insert(:test, {id: 'ida', a: '1', b: '2'})
14
+ @client.insert(:test, {id: 'ida', a: '2', b: '3'})
15
+ @client.insert(:test, {id: 'ida', a: '3', b: '4'})
16
+ @client.insert(:test, {id: 'idb', a: '1', b: '2'})
17
+ @client.insert(:test, {id: 'idb', a: '2', b: '3'})
18
+ end
19
+
20
+ teardown do
21
+ teardown_cass_schema
22
+ end
23
+
24
+ should 'page through cassandra result and return all items' do
25
+ @client.insert(:test, {id: 'ida', a: '4', b: '4'})
26
+
27
+ all_items = AllItemsReader.new(:test, @client)
28
+ result = all_items.pipe(id: 'ida').to_a
29
+ assert_equal 4, result.size
30
+ assert_equal %w(1 2 3 4).to_set, result.map { |i| i['a'] }.to_set
31
+ end
32
+
33
+ context "columns selecting" do
34
+ should "only select given columns from columns argument" do
35
+ out = @pipe.pipe({id: 'idb', :columns => [:a]}).to_a
36
+ assert_equal({'a' => '1'}, out.first)
37
+ assert_equal({'a' => '2'}, out.last)
38
+ end
39
+
40
+ should "select all columns when columns argument not given" do
41
+ out = @pipe.pipe({id: 'idb'}).to_a
42
+ assert_equal({ "id" => "idb", "a" => "1", "b" => "2" }, out.first)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,37 @@
1
+ require 'test_helper'
2
+
3
+ module Pyper::Pipes::Cassandra
4
+ class DeleterTest < Minitest::Should::TestCase
5
+ context 'a cassandra delete pipe' do
6
+ setup do
7
+ setup_cass_schema
8
+
9
+ @client = create_cass_client('test_datastore')
10
+ @deleter = Deleter.new(:test, @client)
11
+ @writer = Writer.new(:test, @client)
12
+ end
13
+
14
+ teardown do
15
+ teardown_cass_schema
16
+ end
17
+
18
+ should 'delete from the specified table' do
19
+ attributes = {id: 'id', a: 'a', b: 'b'}
20
+ @writer.pipe(attributes)
21
+ assert @client.select(:test).execute.first
22
+
23
+ @deleter.pipe({:id => 'id'})
24
+ refute @client.select(:test).execute.first
25
+ end
26
+
27
+ should 'delete certain columns' do
28
+ attributes = {id: 'id', a: 'a', b: 'b'}
29
+ @writer.pipe(attributes)
30
+ assert_equal 'b', @client.select(:test).execute.first['b']
31
+
32
+ @deleter.pipe({:id => 'id', :a => 'a', :columns => [:b]})
33
+ refute @client.select(:test).execute.first['b']
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,47 @@
1
+ require 'test_helper'
2
+
3
+ module Pyper::Pipes::Cassandra
4
+ class ModKeyReaderTest < Minitest::Should::TestCase
5
+ context 'a cassandra reader pipe' do
6
+
7
+ setup do
8
+ setup_cass_schema
9
+
10
+ @client = create_cass_client('test_datastore')
11
+ @pipe = ModKeyReader.new(:mod_test, @client, mod_size = 4)
12
+
13
+ # populate some test data
14
+ @client.insert(:mod_test, {row: 1, id: 'ida', mod_key: 0, val: '1'})
15
+ @client.insert(:mod_test, {row: 1, id: 'idb', mod_key: 1, val: '2'})
16
+ @client.insert(:mod_test, {row: 1, id: 'idc', mod_key: 2, val: '3'})
17
+ end
18
+
19
+ teardown do
20
+ teardown_cass_schema
21
+ end
22
+
23
+ should 'return all items in an unordered enumerator from cassandra' do
24
+ result = @pipe.pipe(row: 1).to_a
25
+ assert_equal 3, result.size
26
+ assert_equal %w(1 2 3).to_set, result.map { |i| i['val'] }.to_set
27
+ end
28
+
29
+ should 'page through cassandra result and return all items' do
30
+ @client.insert(:mod_test, {row: 1, id: 'idd', mod_key: 0, val: '4'})
31
+ @client.insert(:mod_test, {row: 1, id: 'ide', mod_key: 1, val: '5'})
32
+ @client.insert(:mod_test, {row: 1, id: 'idf', mod_key: 2, val: '6'})
33
+
34
+ paging_pipe = ModKeyReader.new(:mod_test, @client, mod_size = 4, page_size = 1)
35
+ result = paging_pipe.pipe(row: 1).to_a
36
+ assert_equal 6, result.size
37
+ assert_equal %w(1 2 3 4 5 6).to_set, result.map { |i| i['val'] }.to_set
38
+ end
39
+
40
+ should 'not return items with a mod key outside the mod key range' do
41
+ @client.insert(:mod_test, {row: 1, id: 'idz', mod_key: 4, val: '1'})
42
+ result = @pipe.pipe(row: 1).to_a
43
+ assert_equal 3, result.size
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,29 @@
1
+ require 'test_helper'
2
+
3
+ module Pyper::Pipes::Cassandra
4
+ class PaginationDecodingTest < Minitest::Should::TestCase
5
+ setup do
6
+ @pipe = PaginationDecoding.new
7
+ end
8
+
9
+ should 'decode the :paging_state argument' do
10
+ decoded = 'sdf'
11
+ encoded = Base64.urlsafe_encode64(decoded)
12
+
13
+ args = {paging_state: encoded}
14
+ new_attrs = @pipe.pipe(args)
15
+ assert_equal decoded, new_attrs[:paging_state]
16
+ end
17
+
18
+ should 'allow missing paging states' do
19
+ args = {}
20
+ new_args = @pipe.pipe(args)
21
+ assert_equal nil, new_args[:paging_state]
22
+ end
23
+
24
+ should 'not modify other args' do
25
+ args = {other: 1}
26
+ assert_equal args, @pipe.pipe(args)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ require 'test_helper'
2
+
3
+ module Pyper::Pipes::Cassandra
4
+ class PaginationEncodingTest < Minitest::Should::TestCase
5
+ setup do
6
+ @pipe = PaginationEncoding.new
7
+ end
8
+
9
+ should 'encode the :paging_state status' do
10
+ state = 'sdf'
11
+ encoded = Base64.urlsafe_encode64(state)
12
+
13
+ status = {paging_state: state}
14
+ @pipe.pipe([], status)
15
+ assert_equal encoded, status[:paging_state]
16
+ end
17
+
18
+ should 'allow missing paging states' do
19
+ status = {}
20
+ @pipe.pipe([], status)
21
+ assert_equal nil, status[:paging_state]
22
+ end
23
+
24
+ should 'not modify the items' do
25
+ items = %w(a b)
26
+ assert_equal items, @pipe.pipe(items, {})
27
+ end
28
+ end
29
+ end