simple-redis-orm 0.1.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/.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
|