pyper_rb 1.2.0

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