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.
@@ -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