Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
@@ -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
+63
View File
@@ -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
+135
View File
@@ -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
+49
View File
@@ -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