pyper_rb 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/Gemfile +24 -0
- data/LICENSE.txt +22 -0
- data/README.md +178 -0
- data/Rakefile +10 -0
- data/lib/pyper/all.rb +4 -0
- data/lib/pyper/pipeline.rb +63 -0
- data/lib/pyper/pipes/cassandra/all_items_reader.rb +40 -0
- data/lib/pyper/pipes/cassandra/deleter.rb +19 -0
- data/lib/pyper/pipes/cassandra/mod_key.rb +32 -0
- data/lib/pyper/pipes/cassandra/mod_key_reader.rb +41 -0
- data/lib/pyper/pipes/cassandra/pagination_decoding.rb +22 -0
- data/lib/pyper/pipes/cassandra/pagination_encoding.rb +17 -0
- data/lib/pyper/pipes/cassandra/reader.rb +35 -0
- data/lib/pyper/pipes/cassandra/writer.rb +24 -0
- data/lib/pyper/pipes/cassandra.rb +8 -0
- data/lib/pyper/pipes/content/fetch.rb +30 -0
- data/lib/pyper/pipes/content/store.rb +36 -0
- data/lib/pyper/pipes/content.rb +2 -0
- data/lib/pyper/pipes/default_values.rb +15 -0
- data/lib/pyper/pipes/field_rename.rb +23 -0
- data/lib/pyper/pipes/force_enumerator.rb +13 -0
- data/lib/pyper/pipes/model/attribute_deserializer.rb +27 -0
- data/lib/pyper/pipes/model/attribute_serializer.rb +34 -0
- data/lib/pyper/pipes/model/attribute_validation.rb +57 -0
- data/lib/pyper/pipes/model/virtus_deserializer.rb +39 -0
- data/lib/pyper/pipes/model/virtus_parser.rb +13 -0
- data/lib/pyper/pipes/model.rb +5 -0
- data/lib/pyper/pipes/no_op.rb +15 -0
- data/lib/pyper/pipes/pry.rb +9 -0
- data/lib/pyper/pipes/remove_fields.rb +22 -0
- data/lib/pyper/pipes.rb +8 -0
- data/lib/pyper/version.rb +3 -0
- data/lib/pyper.rb +4 -0
- data/pyper_rb.gemspec +22 -0
- data/test/fixtures/cass_schema_config.yml +6 -0
- data/test/fixtures/test_datastore/schema.cql +23 -0
- data/test/test_helper.rb +34 -0
- data/test/unit/pyper/pipeline_test.rb +81 -0
- data/test/unit/pyper/pipes/cassandra/all_items_reader_test.rb +47 -0
- data/test/unit/pyper/pipes/cassandra/deleter_test.rb +37 -0
- data/test/unit/pyper/pipes/cassandra/mod_key_reader_test.rb +47 -0
- data/test/unit/pyper/pipes/cassandra/pagination_decoding_test.rb +29 -0
- data/test/unit/pyper/pipes/cassandra/pagination_encoding_test.rb +29 -0
- data/test/unit/pyper/pipes/cassandra/reader_test.rb +79 -0
- data/test/unit/pyper/pipes/cassandra/writer_test.rb +51 -0
- data/test/unit/pyper/pipes/content/fetch_test.rb +38 -0
- data/test/unit/pyper/pipes/content/store_test.rb +49 -0
- data/test/unit/pyper/pipes/field_rename_test.rb +24 -0
- data/test/unit/pyper/pipes/model/attribute_deserializer_test.rb +69 -0
- data/test/unit/pyper/pipes/model/attribute_serializer_test.rb +60 -0
- data/test/unit/pyper/pipes/model/attribute_validation_test.rb +96 -0
- data/test/unit/pyper/pipes/model/virtus_deserializer_test.rb +75 -0
- data/test/unit/pyper/pipes/no_op_test.rb +12 -0
- data/test/unit/pyper/pipes/remove_fields_test.rb +24 -0
- 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,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,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
|
data/lib/pyper/pipes.rb
ADDED
@@ -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'
|
data/lib/pyper.rb
ADDED
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,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
|
+
);
|
data/test/test_helper.rb
ADDED
@@ -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
|