simple-redis-orm 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ module SimpleRedisOrm
2
+ module Types
3
+ include Dry.Types
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleRedisOrm
4
+ VERSION = "0.1.0"
5
+ 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,4 @@
1
+ module SimpleRedisOrm
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ 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,12 @@
1
+ if ENV['RUN_COVERAGE_REPORT']
2
+ require 'simplecov'
3
+
4
+ SimpleCov.start do
5
+ add_filter 'vendor/'
6
+ add_filter %r{^/spec/}
7
+ end
8
+ SimpleCov.minimum_coverage_by_file 90
9
+
10
+ require 'codecov'
11
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
12
+ end
@@ -0,0 +1,9 @@
1
+ RSpec.configure do |config|
2
+ config.before(:suite) do
3
+ SimpleRedisOrm::Entry.redis = MockRedis.new
4
+ end
5
+
6
+ config.before do
7
+ SimpleRedisOrm::Entry.redis.flushdb
8
+ end
9
+ 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,4 @@
1
+ module SimpleRedisOrm
2
+ module Helpers
3
+ end
4
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe SimpleRedisOrm do
4
+ it "has a version number" do
5
+ expect(SimpleRedisOrm::VERSION).not_to be_nil
6
+ end
7
+ end
@@ -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