simple-redis-orm 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +27 -0
- data/.gitignore +20 -0
- data/.rspec +4 -0
- data/.rubocop.yml +238 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +46 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/setup +8 -0
- data/dev/setup.rb +12 -0
- data/lib/simple-redis-orm/application_entry.rb +32 -0
- data/lib/simple-redis-orm/connection_pool_proxy.rb +31 -0
- data/lib/simple-redis-orm/entry.rb +71 -0
- data/lib/simple-redis-orm/helpers/core_commands.rb +96 -0
- data/lib/simple-redis-orm/helpers/serializer.rb +45 -0
- data/lib/simple-redis-orm/types.rb +5 -0
- data/lib/simple-redis-orm/version.rb +5 -0
- data/lib/simple-redis-orm.rb +12 -0
- data/sig/simple-redis-orm.rbs +4 -0
- data/simple-redis-orm.gemspec +36 -0
- data/spec/coverage_helper.rb +12 -0
- data/spec/redis_spec_helper.rb +9 -0
- data/spec/simple-redis-orm/application_entry_spec.rb +55 -0
- data/spec/simple-redis-orm/entry_spec.rb +59 -0
- data/spec/simple-redis-orm/helpers/class_methods_spec.rb +4 -0
- data/spec/simple-redis-orm/helpers/core_commands_spec.rb +73 -0
- data/spec/simple_redis_orm_spec.rb +7 -0
- data/spec/spec_helper.rb +18 -0
- metadata +251 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
module SimpleRedisOrm
|
2
|
+
class NotConnected < StandardError; end
|
3
|
+
|
4
|
+
class Entry < Dry::Struct
|
5
|
+
include Helpers::CoreCommands
|
6
|
+
extend Helpers::CoreCommands::ClassMethods
|
7
|
+
|
8
|
+
attribute :key, Types::String
|
9
|
+
|
10
|
+
def id
|
11
|
+
self.class.without_redis_key_prefix(key)
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def new(id:, **attributes)
|
16
|
+
attrs_with_defaults = fill_in_missing_keys(**attributes)
|
17
|
+
super(key: id, **attrs_with_defaults)
|
18
|
+
end
|
19
|
+
|
20
|
+
def create(id:, **attributes)
|
21
|
+
new(id: id, **attributes).save
|
22
|
+
end
|
23
|
+
|
24
|
+
def find(id)
|
25
|
+
value = read_by(id)
|
26
|
+
return if value.nil?
|
27
|
+
|
28
|
+
new(id: id, **value)
|
29
|
+
end
|
30
|
+
|
31
|
+
def redis
|
32
|
+
@redis || raise(NotConnected, "#{name}.redis not set to a Redis.new connection pool")
|
33
|
+
end
|
34
|
+
|
35
|
+
def redis=(conn)
|
36
|
+
@redis = ConnectionPoolProxy.proxy_if_needed(conn)
|
37
|
+
end
|
38
|
+
|
39
|
+
def without_redis_key_prefix(id)
|
40
|
+
return if id.nil?
|
41
|
+
|
42
|
+
id.gsub(/^(.*):/, '')
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def fill_in_missing_keys(**attributes)
|
48
|
+
keys = attribute_names - [:key]
|
49
|
+
missing_keys = keys - attributes.keys
|
50
|
+
return attributes if missing_keys.empty?
|
51
|
+
|
52
|
+
missing_hash = missing_keys.to_h { |x| [x, nil] }
|
53
|
+
missing_hash.merge(attributes)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def attributes
|
58
|
+
super.except(:key)
|
59
|
+
end
|
60
|
+
|
61
|
+
def redis
|
62
|
+
Entry.redis
|
63
|
+
end
|
64
|
+
|
65
|
+
def save
|
66
|
+
set_hash(attributes.except(:key))
|
67
|
+
|
68
|
+
self
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module SimpleRedisOrm
|
2
|
+
module Helpers
|
3
|
+
module CoreCommands
|
4
|
+
include Serializer
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
include Serializer
|
8
|
+
|
9
|
+
def read_by(key)
|
10
|
+
type = redis.type key
|
11
|
+
return if type.nil? || type == 'none'
|
12
|
+
return read_hash(key) if type == 'hash'
|
13
|
+
|
14
|
+
read_value(key)
|
15
|
+
end
|
16
|
+
|
17
|
+
def read_hash(key)
|
18
|
+
value = redis.hgetall(key)
|
19
|
+
deserialize_value(value)
|
20
|
+
end
|
21
|
+
|
22
|
+
def read_value(key)
|
23
|
+
value = redis.get(key)
|
24
|
+
return if value.nil?
|
25
|
+
|
26
|
+
deserialize_value(value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def read
|
31
|
+
type = redis.type key
|
32
|
+
return if type.nil?
|
33
|
+
|
34
|
+
return read_hash if type == 'hash'
|
35
|
+
|
36
|
+
read_value
|
37
|
+
end
|
38
|
+
|
39
|
+
def read_hash
|
40
|
+
value = redis.hgetall(key)
|
41
|
+
|
42
|
+
deserialize_value(value)
|
43
|
+
end
|
44
|
+
|
45
|
+
def read_value
|
46
|
+
value = redis.get(key)
|
47
|
+
deserialize_value(value)
|
48
|
+
end
|
49
|
+
|
50
|
+
def set(value)
|
51
|
+
return set_hash(value) if value.is_a?(Hash)
|
52
|
+
|
53
|
+
redis.set key, serialize_value(value)
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
# rubocop:disable Naming/AccessorMethodName
|
58
|
+
def set_hash(hash)
|
59
|
+
raise ArgumentError, "'hash_value' must be a hash, found type: #{hash_value.class.name}" unless hash.is_a?(Hash)
|
60
|
+
|
61
|
+
hash.each_pair { |sub_key, value| set_subhash(sub_key, value) }
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
# rubocop:enable Naming/AccessorMethodName
|
65
|
+
|
66
|
+
def exists
|
67
|
+
redis.exists key
|
68
|
+
end
|
69
|
+
|
70
|
+
def exists?
|
71
|
+
redis.exists? key
|
72
|
+
end
|
73
|
+
|
74
|
+
def expire(seconds)
|
75
|
+
redis.expire key, seconds
|
76
|
+
end
|
77
|
+
|
78
|
+
def ttl
|
79
|
+
redis.ttl(key)
|
80
|
+
end
|
81
|
+
|
82
|
+
def read_subhash(field)
|
83
|
+
value = redis.hget(key, field)
|
84
|
+
return if value.nil?
|
85
|
+
|
86
|
+
deserialize_value(value)
|
87
|
+
end
|
88
|
+
|
89
|
+
def set_subhash(field, value)
|
90
|
+
redis.hset(key, field, serialize_value(value))
|
91
|
+
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module SimpleRedisOrm
|
2
|
+
module Helpers
|
3
|
+
module Serializer
|
4
|
+
def serialize_value(value)
|
5
|
+
return value if value.nil?
|
6
|
+
|
7
|
+
MessagePack.pack(value)
|
8
|
+
end
|
9
|
+
|
10
|
+
def deserialize_value(value)
|
11
|
+
return if value.nil?
|
12
|
+
|
13
|
+
return deserialize_hash_value(value) if value.is_a?(Hash)
|
14
|
+
|
15
|
+
MessagePack.unpack(value)
|
16
|
+
end
|
17
|
+
|
18
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
19
|
+
def deserialize_hash_value(hash)
|
20
|
+
return if hash.nil?
|
21
|
+
|
22
|
+
hash.transform_values do |v|
|
23
|
+
next v if v.nil?
|
24
|
+
next v unless v.is_a?(String)
|
25
|
+
next v unless v.encoding == Encoding::ASCII_8BIT
|
26
|
+
|
27
|
+
v.strip.empty? ? v : MessagePack.unpack(v)
|
28
|
+
rescue StandardError => e
|
29
|
+
next without_invalid_characters(v) if e.instance_of?(ArgumentError)
|
30
|
+
|
31
|
+
raise e
|
32
|
+
end.deep_symbolize_keys
|
33
|
+
end
|
34
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def without_invalid_characters(text)
|
39
|
+
return text unless text.is_a?(String)
|
40
|
+
|
41
|
+
text.chars.select(&:valid_encoding?).join
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'active_support/core_ext/hash'
|
3
|
+
require 'active_support/core_ext/string'
|
4
|
+
|
5
|
+
require "simple-redis-orm/version"
|
6
|
+
require "simple-redis-orm/types"
|
7
|
+
require "simple-redis-orm/helpers/serializer"
|
8
|
+
require "simple-redis-orm/helpers/core_commands"
|
9
|
+
require "simple-redis-orm/entry"
|
10
|
+
require "simple-redis-orm/application_entry"
|
11
|
+
|
12
|
+
module SimpleRedisOrm; end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('../lib/simple-redis-orm/version', __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.authors = ["Desmond O'Leary"]
|
7
|
+
gem.email = ["desoleary@gmail.com"]
|
8
|
+
gem.description = %q{Simple ORM backed with redis store}
|
9
|
+
gem.summary = %q{Simple ORM backed with redis store}
|
10
|
+
gem.homepage = "https://github.com/omnitech-solutions/simple-redis-orm"
|
11
|
+
gem.license = "MIT"
|
12
|
+
|
13
|
+
gem.files = `git ls-files`.split($\)
|
14
|
+
gem.executables = gem.files.grep(%r{^exe/}).map{ |f| File.basename(f) }
|
15
|
+
gem.test_files = gem.files.grep(%r{^(spec|features)/})
|
16
|
+
gem.name = "simple-redis-orm"
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
gem.version = SimpleRedisOrm::VERSION
|
19
|
+
gem.required_ruby_version = ">= 2.6.0"
|
20
|
+
|
21
|
+
gem.metadata["homepage_uri"] = gem.homepage
|
22
|
+
gem.metadata["source_code_uri"] = gem.homepage
|
23
|
+
gem.metadata["changelog_uri"] = "#{gem.homepage}/CHANGELOG.md"
|
24
|
+
|
25
|
+
gem.add_runtime_dependency 'activesupport', '~> 6.1', '>= 6.1.7.2'
|
26
|
+
gem.add_runtime_dependency 'redis', '~> 4.0', '>= 4.0'
|
27
|
+
gem.add_runtime_dependency 'dry-struct', '~> 1.6'
|
28
|
+
gem.add_runtime_dependency 'msgpack', '~> 1.6', '>= 1.6'
|
29
|
+
gem.add_runtime_dependency 'connection_pool', '~> 2.3', '>= 2.3'
|
30
|
+
|
31
|
+
gem.add_development_dependency "rake", "~> 13.0.6"
|
32
|
+
gem.add_development_dependency "rspec", "~> 3.12.0"
|
33
|
+
gem.add_development_dependency "simplecov", "~> 0.21.2"
|
34
|
+
gem.add_development_dependency "codecov", "~> 0.6.0"
|
35
|
+
gem.add_development_dependency "mock_redis", "~> 0.36.0"
|
36
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# rubocop:disable RSpec/FilePath
|
2
|
+
#
|
3
|
+
module SimpleRedisOrm
|
4
|
+
RSpec.describe ApplicationEntry do
|
5
|
+
let(:subject_class) do
|
6
|
+
Class.new(ApplicationEntry) do
|
7
|
+
attribute :email, Types::String.optional
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def new(id:, **attributes)
|
11
|
+
super(id: id, **attributes.merge(email: without_redis_key_prefix(id)))
|
12
|
+
end
|
13
|
+
|
14
|
+
def create(email:)
|
15
|
+
attrs = { email: email }
|
16
|
+
|
17
|
+
new(id: email, **attrs).save
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
'SomeModel'
|
22
|
+
end
|
23
|
+
|
24
|
+
def redis
|
25
|
+
Entry.redis
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '.new' do
|
32
|
+
it 'add redis entry' do
|
33
|
+
user = subject_class.new(id: 'desoleary@gmail.com')
|
34
|
+
user.save
|
35
|
+
|
36
|
+
actual = subject_class.find('desoleary@gmail.com')
|
37
|
+
expect(actual.attributes).to eql({ email: 'desoleary@gmail.com' })
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '.create' do
|
42
|
+
let(:email) { 'email@domain.com' }
|
43
|
+
|
44
|
+
it 'add redis entry' do
|
45
|
+
subject_class.create(email: email)
|
46
|
+
|
47
|
+
user = subject_class.find(email)
|
48
|
+
expect(user.key).to eql("some_model:#{email}")
|
49
|
+
expect(user.id).to eql(email)
|
50
|
+
expect(user.email).to eql(email)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
# rubocop:enable RSpec/FilePath
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# rubocop:disable RSpec/FilePath
|
2
|
+
|
3
|
+
module SimpleRedisOrm
|
4
|
+
RSpec.describe Entry do
|
5
|
+
let(:subject_class) do
|
6
|
+
Class.new(Entry) do
|
7
|
+
attribute :password, Types::String.optional
|
8
|
+
|
9
|
+
def self.redis
|
10
|
+
Entry.redis
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:id) { 'some-key' }
|
16
|
+
let(:attributes) { { password: 'some-password' } }
|
17
|
+
let(:instance) { subject_class.new(id: 'desoleary@gmail.com', **attributes) }
|
18
|
+
|
19
|
+
subject(:entry) { instance.save }
|
20
|
+
|
21
|
+
describe '.new' do
|
22
|
+
it 'initializes instance' do
|
23
|
+
instance = subject_class.new(id: id, **attributes)
|
24
|
+
|
25
|
+
expect(instance.id).to eql(id)
|
26
|
+
expect(instance.read).to be_nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '.save' do
|
31
|
+
it 'initializes instance' do
|
32
|
+
instance.save
|
33
|
+
|
34
|
+
expect(instance.read).to eql(attributes)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '.create' do
|
39
|
+
it 'stores record' do
|
40
|
+
actual = subject_class.create(id: id, **attributes)
|
41
|
+
|
42
|
+
expect(actual.id).to eql(id)
|
43
|
+
|
44
|
+
entry = subject_class.find(id)
|
45
|
+
expect(entry.attributes).to eql(attributes)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '.find' do
|
50
|
+
it 'stores hash data into redis store' do
|
51
|
+
entry
|
52
|
+
|
53
|
+
actual = subject_class.find('desoleary@gmail.com')
|
54
|
+
expect(actual.attributes).to eql(attributes)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
# rubocop:enable RSpec/FilePath
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# rubocop:disable RSpec/FilePath
|
2
|
+
module SimpleRedisOrm
|
3
|
+
module Helpers
|
4
|
+
RSpec.describe CoreCommands do
|
5
|
+
let(:subject_class) do
|
6
|
+
Class.new(Dry::Struct) do
|
7
|
+
include CoreCommands
|
8
|
+
extend CoreCommands::ClassMethods
|
9
|
+
|
10
|
+
attribute :key, Types::String
|
11
|
+
|
12
|
+
def redis
|
13
|
+
self.class.redis
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def redis
|
18
|
+
@redis ||= MockRedis.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:key) { 'some-key' }
|
25
|
+
let(:field) { :field }
|
26
|
+
let(:field_value) { 'some-value' }
|
27
|
+
let(:hash_value) { { field => field_value } }
|
28
|
+
let(:other_field) { :other_field }
|
29
|
+
let(:other_value) { 'other_value' }
|
30
|
+
let(:instance) { subject_class.new(key: key) }
|
31
|
+
|
32
|
+
subject(:redis_command) do
|
33
|
+
instance.set_hash(hash_value)
|
34
|
+
instance
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'ClassMethods' do
|
38
|
+
describe '.read_value' do
|
39
|
+
before { instance.set(other_value) }
|
40
|
+
|
41
|
+
it 'returns set simple value' do
|
42
|
+
actual = subject_class.read_value(key)
|
43
|
+
|
44
|
+
expect(actual).to eql(other_value)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '.read_hash' do
|
49
|
+
it 'returns original hash value' do
|
50
|
+
redis_command
|
51
|
+
|
52
|
+
expect(subject_class.read_hash(key)).to eql(hash_value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '.read_by' do
|
57
|
+
it 'returns original hash value' do
|
58
|
+
redis_command
|
59
|
+
|
60
|
+
expect(subject_class.read_by(key)).to eql(hash_value)
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'with unexpected key' do
|
64
|
+
it 'returns original hash value' do
|
65
|
+
expect(subject_class.read_by('some-other-key')).to be_nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
# rubocop:enable RSpec/FilePath
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.join(__dir__, "..", 'dev', 'setup')
|
4
|
+
|
5
|
+
require 'redis_spec_helper'
|
6
|
+
require 'coverage_helper'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
# Enable flags like --only-failures and --next-failure
|
10
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
11
|
+
|
12
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
13
|
+
config.disable_monkey_patching!
|
14
|
+
|
15
|
+
config.expect_with :rspec do |c|
|
16
|
+
c.syntax = :expect
|
17
|
+
end
|
18
|
+
end
|