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