Initial commit
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
require_relative './memory'
|
||||
require_relative './redis'
|
||||
|
||||
module DataRepository
|
||||
module Backend
|
||||
class Detector
|
||||
def initialize(backend_or_connection=nil)
|
||||
@backend_or_connection = backend_or_connection
|
||||
end #initialize
|
||||
|
||||
def detect
|
||||
# TYPE CHECKING OMG!! SEND THE CRAFTSMANSHIPTROOPERS!!
|
||||
return backend_or_connection if is_backend?
|
||||
return Backend::Redis.new(backend_or_connection) if is_redis?
|
||||
return default_backend
|
||||
end #detect
|
||||
|
||||
private
|
||||
|
||||
attr_reader :backend_or_connection
|
||||
|
||||
def default_backend
|
||||
Backend::Memory.new
|
||||
end #default_backend
|
||||
|
||||
def is_backend?
|
||||
!!(backend_or_connection.class.name =~ /^DataRepository::Backend/)
|
||||
end #is_backend?
|
||||
|
||||
def is_redis?
|
||||
backend_or_connection.respond_to? :zremrangebyscore
|
||||
end #is_redis?
|
||||
end # Detector
|
||||
end # Backend
|
||||
end # DataRepository
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
require 'set'
|
||||
require 'json'
|
||||
|
||||
module DataRepository
|
||||
module Backend
|
||||
class Memory < Hash
|
||||
def store(key, data, options={})
|
||||
data = data.to_a if data.is_a?(Set) # OMG FIXME
|
||||
super(key, JSON.parse(data.to_json))
|
||||
end
|
||||
|
||||
def fetch(key, options={})
|
||||
super key
|
||||
end
|
||||
|
||||
def exists?(key)
|
||||
self.has_key?(key)
|
||||
end
|
||||
|
||||
# Not supported, so just call data
|
||||
def transaction(&block)
|
||||
block.call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
require 'redis'
|
||||
require_relative 'redis/set'
|
||||
require_relative 'redis/string'
|
||||
|
||||
module DataRepository
|
||||
module Backend
|
||||
class Redis
|
||||
|
||||
HANDLERS = {
|
||||
'set' => Backend::Redis::Set,
|
||||
'string' => Backend::Redis::String
|
||||
}
|
||||
|
||||
def initialize(redis=::Redis.new)
|
||||
@redis = redis
|
||||
end #initialize
|
||||
|
||||
def store(key, data, options={})
|
||||
persister_for(data).new(redis).store(key, data)
|
||||
expire_in(options.fetch(:expiration, nil), key)
|
||||
end #store
|
||||
|
||||
def fetch(key)
|
||||
return nil unless redis.exists(key)
|
||||
retriever_for(key).new(redis).fetch(key)
|
||||
end #fetch
|
||||
|
||||
def keys
|
||||
redis.keys
|
||||
end #keys
|
||||
|
||||
def exists?(key)
|
||||
redis.exists(key)
|
||||
end #exists?
|
||||
|
||||
def delete(key)
|
||||
redis.del(key)
|
||||
end #delete
|
||||
|
||||
# Not supported, so just call data
|
||||
def transaction(&block)
|
||||
block.call
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :redis
|
||||
|
||||
def expire_in(seconds, key)
|
||||
!!seconds && redis.expire(key, seconds)
|
||||
end #expire_in
|
||||
|
||||
def retriever_for(key)
|
||||
HANDLERS.fetch(redis.type(key), Backend::Redis::String)
|
||||
end #retriever_for
|
||||
|
||||
def persister_for(data)
|
||||
HANDLERS.fetch(data.class.to_s.downcase, Backend::Redis::String)
|
||||
end #persister_for(data)
|
||||
end # Redis
|
||||
end # Backend
|
||||
end # DataRepository
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
require 'redis'
|
||||
|
||||
module DataRepository
|
||||
module Backend
|
||||
class Redis
|
||||
class Set
|
||||
def initialize(redis=Redis.new)
|
||||
@redis = redis
|
||||
end #initialize
|
||||
|
||||
def store(key, data)
|
||||
workaround_until_resque_supports_latest_redis_gem(key, data)
|
||||
end #store
|
||||
|
||||
def fetch(key)
|
||||
redis.smembers key
|
||||
end #fetch
|
||||
|
||||
private
|
||||
|
||||
attr_reader :redis
|
||||
|
||||
def workaround_until_resque_supports_latest_redis_gem(key, data)
|
||||
redis.multi do
|
||||
data.to_a.each { |item| redis.sadd(key, item) }
|
||||
end
|
||||
end #workaround_until_resque_supports_latest_redis_gem
|
||||
end # Set
|
||||
end # Redis
|
||||
end # Backend
|
||||
end # DataRepository
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
require 'json'
|
||||
require 'redis'
|
||||
|
||||
module DataRepository
|
||||
module Backend
|
||||
class Redis
|
||||
class String
|
||||
def initialize(redis=Redis.new)
|
||||
@redis = redis
|
||||
end #initialize
|
||||
|
||||
def store(key, data)
|
||||
redis.set key, data.to_json
|
||||
end #store
|
||||
|
||||
def fetch(key)
|
||||
JSON.parse redis.get(key)
|
||||
end #fetch
|
||||
|
||||
private
|
||||
|
||||
attr_reader :redis
|
||||
end # String
|
||||
end # Redis
|
||||
end # Backend
|
||||
end # DataRepository
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
require 'sequel'
|
||||
require 'uuidtools'
|
||||
|
||||
module DataRepository
|
||||
module Backend
|
||||
class Sequel
|
||||
PAGE = 1
|
||||
PER_PAGE = 300
|
||||
ARRAY_RE = %r{\[.*\]}
|
||||
|
||||
def initialize(db, relation=nil)
|
||||
@db = db
|
||||
@relation = relation.to_sym
|
||||
::Sequel.extension(:pagination)
|
||||
::Sequel.extension(:connection_validator)
|
||||
@db.extension :pg_array if postgres?(@db)
|
||||
end
|
||||
|
||||
def collection(filters={}, available_filters=[])
|
||||
apply_filters(db[relation], filters, available_filters)
|
||||
end
|
||||
|
||||
def store(key, data={})
|
||||
naive_upsert_exposed_to_race_conditions(data)
|
||||
end
|
||||
|
||||
def fetch(value, key=nil)
|
||||
if key.nil?
|
||||
parse( db[relation].where(id: value).first )
|
||||
else
|
||||
parse( db[relation].where(key.to_sym => value).first )
|
||||
end
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
db[relation].where(id: key).delete
|
||||
end
|
||||
|
||||
def next_id
|
||||
UUIDTools::UUID.timestamp_create
|
||||
end
|
||||
|
||||
def apply_filters(dataset, filters={}, available_filters=[])
|
||||
return dataset if filters.nil? || filters.empty?
|
||||
available_filters = symbolize_elements(available_filters)
|
||||
|
||||
filters = symbolize_keys(filters).select { |key, value|
|
||||
available_filters.include?(key)
|
||||
} unless available_filters.empty?
|
||||
|
||||
dataset.where(filters)
|
||||
end
|
||||
|
||||
def paginate(dataset, filter={}, record_count=nil)
|
||||
page, per_page = pagination_params_from(filter)
|
||||
dataset.paginate(page, per_page, record_count)
|
||||
end
|
||||
|
||||
def transaction(&block)
|
||||
db.transaction(&block)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :relation, :db
|
||||
|
||||
def naive_upsert_exposed_to_race_conditions(data={})
|
||||
data = send("serialize_for_#{backend_type}", data)
|
||||
insert(data) unless update(data)
|
||||
end
|
||||
|
||||
def insert(data={})
|
||||
db[relation].insert(data)
|
||||
end
|
||||
|
||||
def update(data={})
|
||||
db[relation].where(id: data.fetch(:id)).update(data) != 0
|
||||
end
|
||||
|
||||
def serialize_for_postgres(data)
|
||||
Hash[
|
||||
data.map { |key, value|
|
||||
value = ::Sequel.pg_array(value) if value.is_a?(Array) && !value.empty?
|
||||
[key, value]
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def serialize_for_other_database(data={})
|
||||
Hash[
|
||||
data.map { |key, value|
|
||||
value = value.to_s if value.is_a?(Array)
|
||||
[key, value]
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def backend_type
|
||||
postgres?(db) ? :postgres : :other_database
|
||||
end
|
||||
|
||||
def postgres?(db)
|
||||
db.database_type == :postgres
|
||||
end
|
||||
|
||||
def parse(attributes={})
|
||||
return unless attributes
|
||||
return attributes if postgres?(db)
|
||||
|
||||
Hash[
|
||||
attributes.map do |key, value|
|
||||
value = JSON.parse(value) if value =~ ARRAY_RE
|
||||
[key, value]
|
||||
end
|
||||
]
|
||||
end
|
||||
|
||||
def symbolize_elements(array=[])
|
||||
array.map { |k| k.to_sym}
|
||||
end
|
||||
|
||||
def symbolize_keys(hash={})
|
||||
Hash[ hash.map { |k, v| [k.to_sym, v] } ]
|
||||
end
|
||||
|
||||
def pagination_params_from(filter)
|
||||
page = (filter.delete(:page) || PAGE).to_i
|
||||
per_page = (filter.delete(:per_page) || PER_PAGE).to_i
|
||||
|
||||
[page, per_page]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
require 'fileutils'
|
||||
|
||||
module DataRepository
|
||||
module Filesystem
|
||||
class Local
|
||||
DEFAULT_PREFIX = File.join(File.dirname(__FILE__), '..', 'tmp')
|
||||
|
||||
def initialize(base_directory=DEFAULT_PREFIX)
|
||||
@base_directory = base_directory
|
||||
end
|
||||
|
||||
def create_base_directory
|
||||
FileUtils.mkpath @base_directory unless exists? @base_directory
|
||||
end
|
||||
|
||||
def store(path, data)
|
||||
FileUtils.mkpath(File.dirname(fullpath_for(path)))
|
||||
|
||||
File.open(fullpath_for(path), 'wb') do |file|
|
||||
data.rewind if data.eof?
|
||||
if data.respond_to?(:bucket)
|
||||
data.read { |chunk| file.write(chunk) }
|
||||
else
|
||||
chunk = data.gets
|
||||
while chunk
|
||||
file.write(chunk)
|
||||
chunk = data.gets
|
||||
end
|
||||
end
|
||||
end
|
||||
path
|
||||
end
|
||||
|
||||
def fetch(path)
|
||||
File.open(fullpath_for(path), 'r')
|
||||
end
|
||||
|
||||
def exists?(path)
|
||||
File.exists?(fullpath_for(path))
|
||||
end
|
||||
|
||||
# Use from controlled environments always
|
||||
def remove(path)
|
||||
if exists?(path)
|
||||
File.delete(fullpath_for(path))
|
||||
end
|
||||
end
|
||||
|
||||
def fullpath_for(path)
|
||||
File.join(base_directory, path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :base_directory
|
||||
|
||||
def targets_for(path)
|
||||
fullpath = fullpath_for(path)
|
||||
[
|
||||
Dir.glob(fullpath),
|
||||
Dir.glob("#{fullpath}/*"),
|
||||
Dir.glob("#{fullpath}/**/*")
|
||||
].flatten
|
||||
.uniq
|
||||
.delete_if { |entry| dot_directory?(entry) }
|
||||
end
|
||||
|
||||
def dot_directory?(path)
|
||||
path == '.' || path == '..'
|
||||
end
|
||||
|
||||
def relative_path_for(path, base_directory)
|
||||
(path.split('/') - base_directory.split('/')).join('/')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
require 'uuidtools'
|
||||
require_relative 'backend/detector'
|
||||
require_relative 'backend/memory'
|
||||
|
||||
module DataRepository
|
||||
def self.new(backend_or_connection=nil)
|
||||
backend = Backend::Detector.new(backend_or_connection).detect
|
||||
Repository.new(backend)
|
||||
end # DataRepository.new
|
||||
|
||||
class Repository
|
||||
def initialize(storage=Backend::Memory.new)
|
||||
@storage = storage
|
||||
end
|
||||
|
||||
def backend
|
||||
@storage
|
||||
end
|
||||
|
||||
def store(key, data, options={})
|
||||
storage.store(key.to_s, data, options)
|
||||
end
|
||||
|
||||
def fetch(key)
|
||||
storage.fetch(key.to_s)
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
storage.delete(key.to_s)
|
||||
end
|
||||
|
||||
def exists?(key)
|
||||
storage.exists?(key)
|
||||
end
|
||||
|
||||
def keys
|
||||
storage.keys
|
||||
end
|
||||
|
||||
def next_id
|
||||
UUIDTools::UUID.timestamp_create
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :storage
|
||||
end # Handler
|
||||
end # DataRepository
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
require 'minitest/autorun'
|
||||
require 'redis'
|
||||
require_relative '../spec_helper'
|
||||
require_relative '../../repository'
|
||||
|
||||
describe DataRepository do
|
||||
describe 'DataRepository.new' do
|
||||
it 'instantiates a repository with a memory backend' do
|
||||
repository = DataRepository.new
|
||||
repository.must_be_instance_of DataRepository::Repository
|
||||
repository.backend.must_be_instance_of DataRepository::Backend::Memory
|
||||
end
|
||||
|
||||
it 'detects the backend if a repository or DB connection is passed' do
|
||||
repository = DataRepository.new(Redis.new)
|
||||
repository.must_be_instance_of DataRepository::Repository
|
||||
repository.backend.must_be_instance_of DataRepository::Backend::Redis
|
||||
end
|
||||
end # DataRepository.new
|
||||
end # DataRepository
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
require 'minitest/autorun'
|
||||
require 'redis'
|
||||
require_relative '../../spec_helper'
|
||||
require_relative '../../../backend/detector'
|
||||
|
||||
include DataRepository
|
||||
|
||||
describe Backend::Detector do
|
||||
describe '#detect' do
|
||||
it 'returns a memory backend if no connection or backend passed' do
|
||||
Backend::Detector.new.detect.must_be_instance_of Backend::Memory
|
||||
end
|
||||
|
||||
it "returns the passed object if it's a backend" do
|
||||
memory_backend = Backend::Memory.new
|
||||
detector = Backend::Detector.new(memory_backend)
|
||||
detector.detect.must_be_instance_of Backend::Memory
|
||||
|
||||
redis_backend = Backend::Redis.new
|
||||
detector = Backend::Detector.new(redis_backend)
|
||||
detector.detect.must_be_instance_of Backend::Redis
|
||||
end
|
||||
|
||||
it 'returns a redis backend if a redis connection is passed' do
|
||||
detector = Backend::Detector.new(Redis.new)
|
||||
detector.detect.must_be_instance_of Backend::Redis
|
||||
end
|
||||
end #detect
|
||||
end # Backend::Detector
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
require 'minitest/autorun'
|
||||
require 'ostruct'
|
||||
require_relative '../../spec_helper'
|
||||
require_relative '../../../repository'
|
||||
require_relative '../../../backend/memory'
|
||||
|
||||
include DataRepository
|
||||
|
||||
describe Repository do
|
||||
before do
|
||||
@repository = Repository.new(Backend::Memory.new)
|
||||
end
|
||||
|
||||
describe '#store' do
|
||||
it 'persists a data structure in the passed key' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
|
||||
@repository.keys.wont_include key.to_s
|
||||
@repository.store(key, data)
|
||||
@repository.keys.must_include key.to_s
|
||||
end
|
||||
|
||||
it 'stringifies symbols in the persisted data structure' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
|
||||
@repository.store(key, data)
|
||||
retrieved_data = @repository.fetch(key)
|
||||
|
||||
retrieved_data.keys.wont_include :id
|
||||
retrieved_data.keys.must_include 'id'
|
||||
end
|
||||
end #store
|
||||
|
||||
describe '#fetch' do
|
||||
it 'retrieves a data structure from a key' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
|
||||
@repository.store(key, data)
|
||||
retrieved_data = @repository.fetch(key.to_s)
|
||||
retrieved_data.fetch('id').must_equal data.fetch(:id)
|
||||
end
|
||||
end #fetch
|
||||
|
||||
describe '#delete' do
|
||||
it 'deletes a key' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
|
||||
@repository.store(key, data)
|
||||
@repository.fetch(key.to_s).wont_be_nil
|
||||
|
||||
@repository.delete(key)
|
||||
lambda { @repository.fetch(key.to_s) }.must_raise KeyError
|
||||
end
|
||||
end #delete
|
||||
|
||||
describe '#keys' do
|
||||
it 'returns all stored keys, stringified' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
|
||||
@repository.store(key, data)
|
||||
@repository.keys.must_equal [key.to_s]
|
||||
end
|
||||
end #keys
|
||||
|
||||
describe '#exists?' do
|
||||
it 'returns if key exists' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
|
||||
@repository.exists?(key.to_s).must_equal false
|
||||
@repository.store(key, data)
|
||||
@repository.exists?(key.to_s).must_equal true
|
||||
end
|
||||
end #exists?
|
||||
end # Repository
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
require 'minitest/autorun'
|
||||
require 'set'
|
||||
require_relative '../../spec_helper'
|
||||
require_relative '../../../backend/redis'
|
||||
require_relative '../../../repository'
|
||||
|
||||
include DataRepository
|
||||
|
||||
describe Backend::Redis do
|
||||
before do
|
||||
@connection = Redis.new
|
||||
@connection.select 8
|
||||
@connection.flushdb
|
||||
|
||||
storage = Backend::Redis.new(@connection)
|
||||
@repository = Repository.new(storage)
|
||||
end
|
||||
|
||||
describe '#store' do
|
||||
it 'persists a data structure in the passed key' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
|
||||
@repository.keys.wont_include key.to_s
|
||||
@repository.store(key, data)
|
||||
@repository.keys.must_include key.to_s
|
||||
|
||||
set = Set.new
|
||||
set.add 1
|
||||
set.add 2
|
||||
|
||||
@repository.store('bogus_key', set)
|
||||
@repository.fetch('bogus_key').must_be_kind_of Enumerable
|
||||
end
|
||||
|
||||
it 'stringifies symbols in the persisted data structe' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
|
||||
@repository.store(key, data)
|
||||
retrieved_data = @repository.fetch(key)
|
||||
|
||||
retrieved_data.keys.wont_include :id
|
||||
retrieved_data.keys.must_include 'id'
|
||||
end
|
||||
|
||||
it 'sets key expiration in seconds if expiration option passed' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
expiration = 1
|
||||
|
||||
@repository.store(key, data, expiration: expiration)
|
||||
retrieved_data = @repository.fetch(key)
|
||||
retrieved_data.keys.must_include 'id'
|
||||
@connection.get(key).wont_be_nil
|
||||
sleep(expiration.to_f + 1.0 / 1.0)
|
||||
@connection.get(key).must_be_nil
|
||||
end
|
||||
end #store
|
||||
|
||||
describe '#fetch' do
|
||||
it 'retrieves a data structure from a key' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
|
||||
@repository.store(key, data)
|
||||
retrieved_data = @repository.fetch(key)
|
||||
|
||||
retrieved_data.fetch('id').must_equal data.fetch(:id)
|
||||
end
|
||||
|
||||
it 'returns nil if key does not exist' do
|
||||
@repository.fetch('non_existent_key').must_equal nil
|
||||
end
|
||||
end #fetch
|
||||
|
||||
describe '#delete' do
|
||||
it 'deletes a key' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
|
||||
@repository.store(key, data)
|
||||
@repository.fetch(key).wont_be_nil
|
||||
|
||||
@repository.delete(key)
|
||||
@repository.fetch(key).must_be_nil
|
||||
end
|
||||
end #delete
|
||||
|
||||
describe '#keys' do
|
||||
it 'returns all stored keys, stringified' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
|
||||
@repository.store(key, data)
|
||||
@repository.keys.must_equal [key.to_s]
|
||||
end
|
||||
end #keys
|
||||
|
||||
describe '#exists?' do
|
||||
it 'returns if key exists' do
|
||||
data = { id: 5 }
|
||||
key = data.fetch(:id)
|
||||
|
||||
@repository.exists?(key.to_s).must_equal false
|
||||
@repository.store(key, data)
|
||||
@repository.exists?(key.to_s).must_equal true
|
||||
end
|
||||
end #exists?
|
||||
end # Backend::Redis
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
require_relative '../../../backend/sequel'
|
||||
require_relative '../../../../../app/models/visualization/member'
|
||||
|
||||
include CartoDB
|
||||
|
||||
describe DataRepository::Backend::Sequel do
|
||||
before do
|
||||
db = SequelRails.connection
|
||||
db.create_table :visualizations do
|
||||
UUID :id, primary_key: true
|
||||
String :name
|
||||
String :display_name
|
||||
String :title
|
||||
String :description
|
||||
String :license
|
||||
String :source
|
||||
String :tags
|
||||
String :map_id
|
||||
String :active_layer_id
|
||||
String :type
|
||||
String :privacy
|
||||
String :encrypted_password
|
||||
String :password_salt
|
||||
UUID :permission_id
|
||||
Boolean :locked
|
||||
String :parent_id
|
||||
String :kind
|
||||
String :prev_id
|
||||
String :next_id
|
||||
String :slide_transition_options
|
||||
String :active_child
|
||||
end
|
||||
|
||||
db.create_table :overlays do
|
||||
String :id, null: false, primary_key: true
|
||||
Integer :order, null: false
|
||||
String :options, text: true
|
||||
String :type
|
||||
String :visualization_id, index: true
|
||||
end
|
||||
|
||||
Visualization.repository = DataRepository::Backend::Sequel.new(db, :visualizations)
|
||||
Overlay.repository = DataRepository::Backend::Sequel.new(db, :overlays)
|
||||
end
|
||||
|
||||
describe '#store' do
|
||||
it 'inserts a visualization' do
|
||||
member = Visualization::Member.new(
|
||||
name: 'visualization 1',
|
||||
tags: ['foo', 'bar']
|
||||
)
|
||||
member.store
|
||||
|
||||
rehydrated_member = Visualization::Member.new(id: member.id)
|
||||
rehydrated_member.fetch
|
||||
rehydrated_member.name.should eq member.name
|
||||
end
|
||||
|
||||
it 'updates the visualization if existing' do
|
||||
member = Visualization::Member.new(
|
||||
name: 'visualization 1',
|
||||
tags: ['foo', 'bar']
|
||||
)
|
||||
member.store
|
||||
Visualization.repository.collection(id: member.id).to_a.size.should eq 1
|
||||
|
||||
member.store
|
||||
Visualization.repository.collection(id: member.id).to_a.size.should eq 1
|
||||
|
||||
Visualization::Member.new(id: member.id).fetch.store
|
||||
Visualization.repository.collection(id: member.id).to_a.size.should eq 1
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delete' do
|
||||
it 'deletes a visualization from persistence' do
|
||||
member = Visualization::Member.new(
|
||||
name: 'visualization 1',
|
||||
tags: ['foo', 'bar']
|
||||
).store
|
||||
|
||||
id = member.id
|
||||
Visualization.repository.fetch(id).nil?.should eq false
|
||||
member.delete
|
||||
Visualization.repository.fetch(id).nil?.should eq true
|
||||
end
|
||||
end
|
||||
|
||||
describe '#collection' do
|
||||
it 'gets a collection of records using the passed filter' do
|
||||
Visualization::Member.new(
|
||||
name: 'visualization 1',
|
||||
map_id: 1
|
||||
).store
|
||||
Visualization::Member.new(
|
||||
name: 'visualization 2',
|
||||
map_id: 1
|
||||
).store
|
||||
|
||||
records = Visualization.repository.collection(map_id: 1)
|
||||
records.to_a.size.should eq 2
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
require 'minitest/autorun'
|
||||
require 'stringio'
|
||||
require_relative '../../../filesystem/local'
|
||||
|
||||
include DataRepository::Filesystem
|
||||
|
||||
describe Local do
|
||||
before do
|
||||
@data = StringIO.new(Time.now.to_f.to_s)
|
||||
@path = File.join( (0..2).map { Time.now.to_f.to_s } )
|
||||
@prefix = File.join(Local::DEFAULT_PREFIX, Time.now.to_i.to_s)
|
||||
end
|
||||
|
||||
after do
|
||||
@data.close
|
||||
FileUtils.rmtree(@prefix)
|
||||
FileUtils.rmtree(Local::DEFAULT_PREFIX)
|
||||
end
|
||||
|
||||
describe '#initialize' do
|
||||
it 'sets the storage prefix to Local::DEFAULT_PREFIX by default' do
|
||||
File.exists?( File.join(Local::DEFAULT_PREFIX, @path) ).must_equal false
|
||||
|
||||
path = Local.new.store(@path, @data)
|
||||
File.exists?( File.join(Local::DEFAULT_PREFIX, @path) ).must_equal true
|
||||
end
|
||||
end #initialize
|
||||
|
||||
describe '#store' do
|
||||
it 'stores data in the specified path' do
|
||||
filesystem = Local.new(@prefix)
|
||||
path = filesystem.store(@path, @data)
|
||||
|
||||
@data.rewind
|
||||
|
||||
stored_data = File.open( File.join(@prefix, path) )
|
||||
stored_data.read.must_equal @data.read
|
||||
end
|
||||
end #store
|
||||
|
||||
describe '#fetch' do
|
||||
it 'retrieves data from the specified path' do
|
||||
filesystem = Local.new(@prefix)
|
||||
path = filesystem.store(@path, @data)
|
||||
|
||||
@data.rewind
|
||||
|
||||
stored_data = Local.new(@prefix).fetch(path)
|
||||
stored_data.read.must_equal @data.read
|
||||
end
|
||||
end #fetch
|
||||
end # Local
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
require 'minitest/autorun'
|
||||
require_relative '../../../structures/collection'
|
||||
|
||||
include DataRepository
|
||||
|
||||
describe Collection do
|
||||
before do
|
||||
@repository = DataRepository::Repository.new
|
||||
@dummy_class = Class.new do
|
||||
attr_accessor :id
|
||||
def initialize(arguments={}); self.id = arguments.fetch(:id); end
|
||||
def fetch; self; end
|
||||
def to_hash; { id: id }; end
|
||||
def ==(other); id.to_s == other.id.to_s; end
|
||||
end
|
||||
|
||||
@defaults = { repository: @repository, member_class: @dummy_class}
|
||||
end
|
||||
|
||||
describe '#add' do
|
||||
it 'adds a member to the collection' do
|
||||
member = @dummy_class.new(id: 1)
|
||||
collection = Collection.new(@defaults)
|
||||
collection.add(member)
|
||||
collection.to_a.must_include member
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delete' do
|
||||
it 'deletes a member from the collection' do
|
||||
member = @dummy_class.new(id: 1)
|
||||
collection = Collection.new(@defaults)
|
||||
collection.add(member)
|
||||
collection.delete(member)
|
||||
collection.to_a.wont_include member
|
||||
end
|
||||
end #delete
|
||||
|
||||
describe '#each' do
|
||||
it 'yields members of the collection as the initialized member_class' do
|
||||
member = @dummy_class.new(id: 1)
|
||||
collection = Collection.new(@defaults)
|
||||
collection.add(member)
|
||||
collection.store
|
||||
|
||||
rehydrated_collection =
|
||||
Collection.new(@defaults.merge(signature: collection.signature))
|
||||
rehydrated_collection.fetch
|
||||
rehydrated_collection.to_a.first.must_be_instance_of @dummy_class
|
||||
end
|
||||
|
||||
it 'returns an enumerator if no block given' do
|
||||
member = @dummy_class.new(id: 1)
|
||||
collection = Collection.new({ repository: @repository })
|
||||
collection.add(member)
|
||||
collection.store
|
||||
|
||||
rehydrated_collection =
|
||||
Collection.new(@defaults.merge(signature: collection.signature))
|
||||
rehydrated_collection.fetch
|
||||
|
||||
enumerator = rehydrated_collection.each
|
||||
enumerator.next.must_be_instance_of @dummy_class
|
||||
end
|
||||
end #each
|
||||
|
||||
describe '#fetch' do
|
||||
it 'resets the collection with data from the data repository' do
|
||||
member1 = @dummy_class.new(id: 1)
|
||||
member2 = @dummy_class.new(id: 2)
|
||||
collection = Collection.new(@defaults)
|
||||
collection.add(member1)
|
||||
collection.store
|
||||
|
||||
rehydrated_collection =
|
||||
Collection.new(@defaults.merge(signature: collection.signature))
|
||||
rehydrated_collection.add(member2)
|
||||
|
||||
rehydrated_collection.to_a.must_include(member2)
|
||||
rehydrated_collection.to_a.wont_include(member1)
|
||||
rehydrated_collection.fetch
|
||||
rehydrated_collection.to_a.must_include(member1)
|
||||
rehydrated_collection.to_a.wont_include(member2)
|
||||
end
|
||||
|
||||
it 'empties the collection if it was not persisted to the repository' do
|
||||
member = @dummy_class.new(id: 1)
|
||||
collection = Collection.new(@defaults)
|
||||
collection.add(member)
|
||||
collection.to_a.length.must_equal 1
|
||||
collection.fetch
|
||||
collection.to_a.must_be_empty
|
||||
end
|
||||
end #fetch
|
||||
|
||||
describe '#store' do
|
||||
it 'persists the collection to the data repository' do
|
||||
member = @dummy_class.new(id: 1)
|
||||
collection = Collection.new(@defaults)
|
||||
collection.add(member)
|
||||
collection.store
|
||||
|
||||
rehydrated_collection =
|
||||
Collection.new(@defaults.merge(signature: collection.signature))
|
||||
rehydrated_collection.fetch
|
||||
rehydrated_collection.map { |member| member.id }.must_include member.id
|
||||
end
|
||||
end #store
|
||||
|
||||
describe '#to_json' do
|
||||
it 'renders a JSON representation of the collection' do
|
||||
member = @dummy_class.new(id: 1)
|
||||
collection = Collection.new(@defaults)
|
||||
collection.add(member)
|
||||
collection.store
|
||||
|
||||
representation = JSON.parse(collection.to_json)
|
||||
representation.size.must_equal 1
|
||||
representation.first.fetch('id').must_equal member.id
|
||||
end
|
||||
end #to_json
|
||||
end # Collection
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
require 'ostruct'
|
||||
require 'set'
|
||||
require_relative '../repository'
|
||||
|
||||
module DataRepository
|
||||
class Collection
|
||||
include Enumerable
|
||||
|
||||
INTERFACE = %w{ signature add delete store fetch each to_json repository count } + Enumerable.instance_methods
|
||||
|
||||
attr_reader :signature
|
||||
attr_accessor :storage
|
||||
|
||||
def initialize(arguments={})
|
||||
@storage = Set.new
|
||||
@member_class = arguments.fetch(:member_class, OpenStruct)
|
||||
@repository = arguments.fetch(:repository, Repository.new)
|
||||
@signature = arguments.fetch(:signature, @repository.next_id)
|
||||
end #initialize
|
||||
|
||||
def add(member)
|
||||
storage.add(member)
|
||||
self
|
||||
end #add
|
||||
|
||||
def delete(member)
|
||||
storage.delete(member)
|
||||
self
|
||||
end #delete
|
||||
|
||||
def delete_if(&block)
|
||||
storage.delete_if(&block)
|
||||
self
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
return storage.each(&block)
|
||||
Enumerator.new(self, :each)
|
||||
end #each
|
||||
|
||||
def fetch
|
||||
self.storage = Set[*repository.fetch(signature)].map do |attributes|
|
||||
puts attributes.inspect
|
||||
member_class.new(attributes)
|
||||
end
|
||||
|
||||
self
|
||||
rescue => exception
|
||||
storage.clear
|
||||
self
|
||||
end #fetch
|
||||
|
||||
def store
|
||||
repository.store(signature, storage.map(&:id).to_a)
|
||||
self
|
||||
end #store
|
||||
|
||||
def to_json(*args)
|
||||
map { |member| member.to_hash }.to_json(*args)
|
||||
end #to_json
|
||||
|
||||
def count
|
||||
storage.count
|
||||
end #count
|
||||
|
||||
private
|
||||
|
||||
attr_reader :repository, :member_class
|
||||
|
||||
def members
|
||||
storage.each { |member_id| yield member_class.new(id: member_id).fetch }
|
||||
end #members
|
||||
end # Collection
|
||||
end # DataRepository
|
||||
|
||||
Reference in New Issue
Block a user