red_matryoshka 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8566777fcb1de0f09b3074e43d3fd1a0f22b42a2
4
+ data.tar.gz: 59dd1db49c24141c8ac3e0b9616a4ec9fd4643d5
5
+ SHA512:
6
+ metadata.gz: ba454fd735f2d82c4685d1a7aecfb840dd65a27f6d61fa1b6241d6f3b51b515d5af51fa368b37de1a7f335efa6b61d55b30c5f694a0b858e65a8aaf334a99b63
7
+ data.tar.gz: e3ed008f90f50949c16fc48883b424d03a959a0357f73126ca0c7aa7e5d637917f001b795bccf223ac67cfc69f33ff1782260aba8b260f117403376fbbb79dc2
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1 @@
1
+ inherit_from: .ruby-style.yml
@@ -0,0 +1,258 @@
1
+ AllCops:
2
+ Exclude:
3
+ - "bin/**/*"
4
+ - "config/environments/**/*"
5
+ - "config/initializers/**/*"
6
+ - "db/migrate/**/*"
7
+ - "db/schema.rb"
8
+ - "lib/tasks/**/*"
9
+ - "vendor/**/*"
10
+ - "red_matryoshka.gemspec"
11
+ UseCache: false
12
+ Style/CollectionMethods:
13
+ Description: Preferred collection methods.
14
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#map-find-select-reduce-size
15
+ Enabled: true
16
+ PreferredMethods:
17
+ collect: map
18
+ collect!: map!
19
+ find: detect
20
+ find_all: select
21
+ reduce: inject
22
+ Style/DotPosition:
23
+ Description: Checks the position of the dot in multi-line method calls.
24
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains
25
+ Enabled: true
26
+ EnforcedStyle: trailing
27
+ SupportedStyles:
28
+ - leading
29
+ - trailing
30
+ Style/FileName:
31
+ Description: Use snake_case for source file names.
32
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#snake-case-files
33
+ Enabled: false
34
+ Exclude: []
35
+ Style/GuardClause:
36
+ Description: Check for conditionals that can be replaced with guard clauses
37
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals
38
+ Enabled: false
39
+ MinBodyLength: 1
40
+ Style/IfUnlessModifier:
41
+ Description: Favor modifier if/unless usage when you have a single-line body.
42
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier
43
+ Enabled: false
44
+ MaxLineLength: 100
45
+ Style/OptionHash:
46
+ Description: Don't use option hashes when you can use keyword arguments.
47
+ Enabled: false
48
+ Style/PercentLiteralDelimiters:
49
+ Description: Use `%`-literal delimiters consistently
50
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#percent-literal-braces
51
+ Enabled: false
52
+ PreferredDelimiters:
53
+ "%": "()"
54
+ "%i": "()"
55
+ "%q": "()"
56
+ "%Q": "()"
57
+ "%r": "{}"
58
+ "%s": "()"
59
+ "%w": "()"
60
+ "%W": "()"
61
+ "%x": "()"
62
+ Style/PredicateName:
63
+ Description: Check the names of predicate methods.
64
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark
65
+ Enabled: true
66
+ NamePrefix:
67
+ - is_
68
+ - has_
69
+ - have_
70
+ NamePrefixBlacklist:
71
+ - is_
72
+ Exclude:
73
+ - spec/**/*
74
+ Style/RaiseArgs:
75
+ Description: Checks the arguments passed to raise/fail.
76
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#exception-class-messages
77
+ Enabled: false
78
+ EnforcedStyle: exploded
79
+ SupportedStyles:
80
+ - compact
81
+ - exploded
82
+ Style/SignalException:
83
+ Description: Checks for proper usage of fail and raise.
84
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#fail-method
85
+ Enabled: false
86
+ EnforcedStyle: semantic
87
+ SupportedStyles:
88
+ - only_raise
89
+ - only_fail
90
+ - semantic
91
+ Style/SingleLineBlockParams:
92
+ Description: Enforces the names of some block params.
93
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#reduce-blocks
94
+ Enabled: false
95
+ Methods:
96
+ - reduce:
97
+ - a
98
+ - e
99
+ - inject:
100
+ - a
101
+ - e
102
+ Style/SingleLineMethods:
103
+ Description: Avoid single-line methods.
104
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-single-line-methods
105
+ Enabled: false
106
+ AllowIfMethodIsEmpty: true
107
+ Style/StringLiterals:
108
+ Description: Checks if uses of quotes match the configured preference.
109
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#consistent-string-literals
110
+ Enabled: true
111
+ EnforcedStyle: double_quotes
112
+ SupportedStyles:
113
+ - single_quotes
114
+ - double_quotes
115
+ Style/StringLiteralsInInterpolation:
116
+ Description: Checks if uses of quotes inside expressions in interpolated strings
117
+ match the configured preference.
118
+ Enabled: true
119
+ EnforcedStyle: single_quotes
120
+ SupportedStyles:
121
+ - single_quotes
122
+ - double_quotes
123
+ Style/TrailingCommaInArguments:
124
+ Description: 'Checks for trailing comma in argument lists.'
125
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
126
+ Enabled: false
127
+ EnforcedStyleForMultiline: no_comma
128
+ SupportedStyles:
129
+ - comma
130
+ - consistent_comma
131
+ - no_comma
132
+ Style/TrailingCommaInLiteral:
133
+ Description: 'Checks for trailing comma in array and hash literals.'
134
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
135
+ Enabled: false
136
+ EnforcedStyleForMultiline: no_comma
137
+ SupportedStyles:
138
+ - comma
139
+ - consistent_comma
140
+ - no_comma
141
+ Metrics/AbcSize:
142
+ Description: A calculated magnitude based on number of assignments, branches, and
143
+ conditions.
144
+ Enabled: false
145
+ Max: 15
146
+ Metrics/ClassLength:
147
+ Description: Avoid classes longer than 100 lines of code.
148
+ Enabled: false
149
+ CountComments: false
150
+ Max: 100
151
+ Metrics/ModuleLength:
152
+ CountComments: false
153
+ Max: 100
154
+ Description: Avoid modules longer than 100 lines of code.
155
+ Enabled: false
156
+ Metrics/CyclomaticComplexity:
157
+ Description: A complexity metric that is strongly correlated to the number of test
158
+ cases needed to validate a method.
159
+ Enabled: false
160
+ Max: 6
161
+ Metrics/LineLength:
162
+ Description: Limit lines to 100 characters.
163
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#80-character-limits
164
+ Enabled: true
165
+ Max: 100
166
+ AllowURI: true
167
+ URISchemes:
168
+ - http
169
+ - https
170
+ Metrics/MethodLength:
171
+ Description: Avoid methods longer than 10 lines of code.
172
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#short-methods
173
+ Enabled: false
174
+ CountComments: false
175
+ Max: 10
176
+ Metrics/ParameterLists:
177
+ Description: Avoid parameter lists longer than three or four parameters.
178
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#too-many-params
179
+ Enabled: false
180
+ Max: 5
181
+ CountKeywordArgs: true
182
+ Metrics/PerceivedComplexity:
183
+ Description: A complexity metric geared towards measuring complexity for a human
184
+ reader.
185
+ Enabled: false
186
+ Max: 7
187
+ Lint/AssignmentInCondition:
188
+ Description: Don't use assignment in conditions.
189
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition
190
+ Enabled: false
191
+ AllowSafeAssignment: true
192
+ Style/InlineComment:
193
+ Description: Avoid inline comments.
194
+ Enabled: false
195
+ Style/AccessorMethodName:
196
+ Description: Check the naming of accessor methods for get_/set_.
197
+ Enabled: false
198
+ Style/Alias:
199
+ Description: Use alias_method instead of alias.
200
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#alias-method
201
+ Enabled: false
202
+ Style/Documentation:
203
+ Description: Document classes and non-namespace modules.
204
+ Enabled: false
205
+ Style/DoubleNegation:
206
+ Description: Checks for uses of double negation (!!).
207
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-bang-bang
208
+ Enabled: false
209
+ Style/EachWithObject:
210
+ Description: Prefer `each_with_object` over `inject` or `reduce`.
211
+ Enabled: false
212
+ Style/EmptyLiteral:
213
+ Description: Prefer literals to Array.new/Hash.new/String.new.
214
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#literal-array-hash
215
+ Enabled: false
216
+ Style/ModuleFunction:
217
+ Description: Checks for usage of `extend self` in modules.
218
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#module-function
219
+ Enabled: false
220
+ Style/OneLineConditional:
221
+ Description: Favor the ternary operator(?:) over if/then/else/end constructs.
222
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#ternary-operator
223
+ Enabled: false
224
+ Style/PerlBackrefs:
225
+ Description: Avoid Perl-style regex back references.
226
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers
227
+ Enabled: false
228
+ Style/Send:
229
+ Description: Prefer `Object#__send__` or `Object#public_send` to `send`, as `send`
230
+ may overlap with existing methods.
231
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#prefer-public-send
232
+ Enabled: false
233
+ Style/SpecialGlobalVars:
234
+ Description: Avoid Perl-style global variables.
235
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms
236
+ Enabled: false
237
+ Style/VariableInterpolation:
238
+ Description: Don't interpolate global, instance and class variables directly in
239
+ strings.
240
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#curlies-interpolate
241
+ Enabled: false
242
+ Style/WhenThen:
243
+ Description: Use when x then ... for one-line cases.
244
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#one-line-cases
245
+ Enabled: false
246
+ Lint/EachWithObjectArgument:
247
+ Description: Check for immutable argument given to each_with_object.
248
+ Enabled: true
249
+ Lint/HandleExceptions:
250
+ Description: Don't suppress exception.
251
+ StyleGuide: https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions
252
+ Enabled: false
253
+ Lint/LiteralInCondition:
254
+ Description: Checks of literals used in conditions.
255
+ Enabled: false
256
+ Lint/LiteralInInterpolation:
257
+ Description: Checks for literals used in interpolation.
258
+ Enabled: false
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.0
5
+ before_install: gem install bundler -v 1.12.4
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at charles@crew.co. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in red_matryoshka.gemspec
4
+ gemspec
5
+ rails_version = "~> 4.2.6"
6
+
7
+ gem "coveralls", require: false, group: :test
8
+ gem "activerecord", rails_version
9
+
10
+ group :development, :test do
11
+ gem "rubocop", require: false
12
+ end
13
+
14
+ group :test do
15
+ gem "sqlite3"
16
+ gem "database_cleaner"
17
+ end
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Charles Lalonde
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,44 @@
1
+ # RedMatryoshka
2
+
3
+ [![Coverage Status](https://coveralls.io/repos/github/unsplash/red_matryoshka/badge.svg?branch=master&t=1y1uR2)](https://coveralls.io/github/unsplash/red_matryoshka?branch=master)
4
+ [ ![Codeship Status for unsplash/red_matryoshka](https://codeship.com/projects/cbdfc200-098d-0134-abe7-2adbeb910e90/status?branch=master)](https://codeship.com/projects/155312)
5
+
6
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/red_matryoshka`. To experiment with that code, run `bin/console` for an interactive prompt.
7
+
8
+ TODO: Delete this and the text above, and describe your gem
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'red_matryoshka'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install red_matryoshka
25
+
26
+ ## Usage
27
+
28
+ TODO: Write usage instructions here
29
+
30
+ ## Development
31
+
32
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
33
+
34
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
35
+
36
+ ## Contributing
37
+
38
+ Bug reports and pull requests are welcome on GitHub at https://github.com/unsplash/red_matryoshka. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
39
+
40
+
41
+ ## License
42
+
43
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
44
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "red_matryoshka"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,22 @@
1
+ require "red_matryoshka/version"
2
+ require "red_matryoshka/config"
3
+ require "red_matryoshka/base"
4
+ require "red_matryoshka/cache"
5
+ require "red_matryoshka/doll"
6
+ require "red_matryoshka/related_cache"
7
+
8
+ require "redis"
9
+
10
+ module RedMatryoshka
11
+ class << self
12
+ attr_accessor :configuration
13
+ end
14
+
15
+ def self.configuration
16
+ @configuration ||= Config.new
17
+ end
18
+
19
+ def self.configure
20
+ yield(configuration)
21
+ end
22
+ end
@@ -0,0 +1,133 @@
1
+ module RedMatryoshka
2
+ class Base
3
+ attr_accessor :sub_id
4
+ @sub_id = nil
5
+
6
+ def set_sub_id(sub_id)
7
+ @sub_id = sub_id
8
+ self
9
+ end
10
+
11
+ def class_to_fetch_from
12
+ @class_to_fetch_from ||= self.class.class_to_fetch_from
13
+ end
14
+
15
+ def classes_to_depends_on
16
+ @classes_to_depends_on ||= self.class.cache_classes_to_depends_on || {}
17
+ end
18
+
19
+ def class_to_depends_on_with_dependencies
20
+ @class_to_depends_on_with_dependencies ||= begin
21
+ classes_to_depends_on.inject({}) do |hashes, (k, v)|
22
+ sub_dependencies = {
23
+ sub_dependencies: cache_klass_with_module(k).constantize.new.classes_to_depends_on
24
+ }
25
+ hashes.merge! k => v.merge(sub_dependencies)
26
+ end
27
+ end
28
+ end
29
+
30
+ def classes_dependencies_of
31
+ @cache_classes_dependencies ||= self.class.cache_classes_dependencies || []
32
+ end
33
+
34
+ def classes_to_listen_for
35
+ default_listen_for = { self.class.class_to_fetch_from => { sub: nil, record: :self } }
36
+ @classes_to_listen_for ||= self.class.classes_to_listen_for || default_listen_for
37
+ end
38
+
39
+ def to_hash(object)
40
+ object.attributes.symbolize_keys
41
+ end
42
+
43
+ def after_fetch(hashes)
44
+ # Instantiate me if you want to edit data
45
+ hashes
46
+ end
47
+
48
+ def cache_key(id, prefix = true)
49
+ key_prefix = RedMatryoshka.configuration.key_prefix
50
+ klass_name = self.class.name.demodulize # Only retrieve class without module name
51
+
52
+ key_parts = klass_name.split(":")
53
+ key_parts << @sub_id unless @sub_id.nil?
54
+ if prefix && key_parts[0].casecmp(key_prefix) != 0
55
+ key_parts.insert(0, key_prefix)
56
+ end
57
+
58
+ key_parts = key_parts.map do |part|
59
+ part.to_s.split(/(?=[A-Z])/).map(&:downcase).join("_")
60
+ end
61
+
62
+ key_parts << id
63
+ key_parts.join(":")
64
+ end
65
+
66
+ def cache_klass_with_module(klass)
67
+ if RedMatryoshka.configuration.module.present?
68
+ "#{RedMatryoshka.configuration.module}::#{klass}"
69
+ else
70
+ klass
71
+ end
72
+ end
73
+
74
+ class << self
75
+ DEFAULT_DEPENDS_ON_OPTIONS = { for: nil, as: :hash }.freeze
76
+ DEFAULT_LISTEN_FOR_OPTIONS = { sub: nil, record: :self }.freeze
77
+ attr_reader :class_to_fetch_from,
78
+ :cache_classes_to_depends_on,
79
+ :cache_classes_dependencies,
80
+ :classes_to_listen_for
81
+
82
+ def fetch(*ids)
83
+ RedMatryoshka::Cache.new(new).fetch(ids)
84
+ end
85
+
86
+ def fetch_sub(sub_id, ids = [])
87
+ klass = new.set_sub_id sub_id
88
+
89
+ RedMatryoshka::Cache.new(klass).fetch(ids)
90
+ end
91
+
92
+ def fetch_from(klass)
93
+ @class_to_fetch_from = transformed_class klass
94
+ end
95
+
96
+ def listen_for(klass, options = {})
97
+ @classes_to_listen_for = {} if @classes_to_listen_for.nil?
98
+
99
+ klass_options = DEFAULT_LISTEN_FOR_OPTIONS.merge options
100
+ @classes_to_listen_for[transformed_class(klass)] = klass_options
101
+ end
102
+
103
+ def depends_on(klass, options = {})
104
+ klass_to_depends_on = transformed_class klass
105
+
106
+ @cache_classes_to_depends_on = {} if @cache_classes_to_depends_on.nil?
107
+
108
+ unless @cache_classes_to_depends_on.include? klass_to_depends_on
109
+ klass_options = DEFAULT_DEPENDS_ON_OPTIONS.merge options
110
+ @cache_classes_to_depends_on[klass_to_depends_on] = klass_options
111
+ end
112
+ end
113
+
114
+ def depends_on_multiple(klass, options = {})
115
+ for_option = options[:for] || nil
116
+ depends_on(klass, options.merge(for: for_option, as: :array))
117
+ end
118
+
119
+ def dependency_of(klass)
120
+ klass_that_depends_of = transformed_class klass
121
+
122
+ @cache_classes_dependencies = [] if @cache_classes_dependencies.nil?
123
+ unless @cache_classes_dependencies.include? klass_that_depends_of
124
+ @cache_classes_dependencies << klass_that_depends_of
125
+ end
126
+ end
127
+
128
+ def transformed_class(klass)
129
+ klass.to_s.split("_").map(&:capitalize).join
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,160 @@
1
+ module RedMatryoshka
2
+ class Cache
3
+ def initialize(cache_klass)
4
+ @cache_klass = cache_klass
5
+ end
6
+
7
+ def fetch(ids)
8
+ @cache_klass.after_fetch(
9
+ fetch_objects(ids)
10
+ )
11
+ end
12
+
13
+ def cache(object)
14
+ key = cache_keys(object.id)
15
+
16
+ doll = cache_doll(object)
17
+
18
+ RedMatryoshka.configuration.redis.mapped_hmset key, doll.flatten
19
+
20
+ doll.expand
21
+ end
22
+
23
+ # TODO: Right a test only for this method
24
+ def cache_doll(object)
25
+ RedMatryoshka::Doll.new(
26
+ merge_dependent_hashes_and_main_hash(
27
+ @cache_klass.to_hash(object),
28
+ associate_object(object)
29
+ ),
30
+ @cache_klass
31
+ )
32
+ end
33
+
34
+ def delete(object_id)
35
+ key = cache_keys(object_id)
36
+ RedMatryoshka.configuration.redis.del key
37
+ end
38
+
39
+ private
40
+
41
+ def fetch_objects(ids)
42
+ ids = [ids] unless ids.is_a? Array
43
+ ids = ids[0] if ids[0].is_a? Array
44
+
45
+ hashes = object_from_cache(*ids)
46
+ keys_to_retrieve = object_id_to_retrieve_from_db(ids, hashes)
47
+ fetch_objects_from_db(keys_to_retrieve).each do |object|
48
+ i = ids.find_index(object.id)
49
+ hashes[i] = cache(object)
50
+ end
51
+
52
+ hashes.map { |hash| RedMatryoshka::Doll.new(hash, @cache_klass).expand }
53
+ end
54
+
55
+ def object_from_cache(*object_ids)
56
+ keys = cache_keys(*object_ids)
57
+ keys = [keys] if keys.is_a? String
58
+
59
+ RedMatryoshka.configuration.redis.multi do |redis|
60
+ keys.each do |key|
61
+ redis.hgetall(key)
62
+ end
63
+ end
64
+ end
65
+
66
+ def object_id_to_retrieve_from_db(object_ids, redis_objects_hash)
67
+ redis_objects_hash.each_with_index.inject([]) do |keys, (v, i)|
68
+ keys << object_ids[i] if v.empty?
69
+ keys # Force return to avoid case where `keys` is empty
70
+ end
71
+ end
72
+
73
+ def fetch_objects_from_db(object_ids_to_fetch = [])
74
+ return [] unless object_ids_to_fetch.any?
75
+ @cache_klass.class_to_fetch_from.constantize.find(object_ids_to_fetch)
76
+ end
77
+
78
+ def cache_keys(*object_ids)
79
+ object_ids.map do |id|
80
+ @cache_klass.cache_key id
81
+ end
82
+ end
83
+
84
+ def merge_dependent_hashes_and_main_hash(hash, dependent_hashes)
85
+ if dependent_hashes.any? && dependent_hashes.keys.any?
86
+ dependent_hashes.each do |dependence|
87
+ if dependent_hashes[dependence[0]].is_a?(Array) && dependent_hashes[dependence[0]].any?
88
+ hash.merge! dependent_hashes[dependence[0]].inject(&:merge)
89
+ elsif dependent_hashes[dependence[0]].is_a? Hash
90
+ hash.merge! dependent_hashes[dependence[0]]
91
+ end
92
+
93
+ hash.delete dependence[0]
94
+ end
95
+ end
96
+
97
+ hash
98
+ end
99
+
100
+ def associate_object(object)
101
+ @cache_klass.classes_to_depends_on.inject({}) do |associates, dependence|
102
+ d = associate_object_id(dependence)
103
+ d_id = @cache_klass.to_hash(object)[associate_object_id(dependence)]
104
+
105
+ return associates if d_id.nil?
106
+
107
+ associate_object = associate_object_key_formatter(
108
+ transform_class_name(dependence[0]),
109
+ d_id,
110
+ self.class.new(
111
+ cache_klass_with_module(dependence[0]).constantize.new
112
+ ).send(:fetch_objects, d_id)
113
+ )
114
+
115
+ associates.merge(d => associate_object)
116
+ end
117
+ end
118
+
119
+ def associate_object_field(dependence)
120
+ base_fetch_class = cache_klass_with_module(dependence[0]).constantize.class_to_fetch_from
121
+ associate_id = dependence[1][:for] || base_fetch_class.to_s + "_id"
122
+
123
+ transform_class_name(associate_id)
124
+ rescue NameError
125
+ base_fetch_class.to_s + "_id"
126
+ end
127
+
128
+ def associate_object_id(dependence)
129
+ associate_object_field(dependence).to_sym
130
+ end
131
+
132
+ def associate_object_key_formatter(dependence_name, dependence_id, fetch_object)
133
+ if dependence_id.is_a? Array
134
+ dependence_id.map.with_index do |id, i|
135
+ object_formatter(dependence_name, id, fetch_object[i])
136
+ end
137
+ else
138
+ object_formatter(dependence_name, dependence_id, fetch_object.first)
139
+ end
140
+ end
141
+
142
+ def object_formatter(dependence_name, dependence_id, fetch_object)
143
+ RedMatryoshka::Doll.new(fetch_object).flatten.inject({}) do |format_hash, (k, v)|
144
+ format_hash.merge "#{dependence_name}:#{dependence_id}:#{k}" => v
145
+ end
146
+ end
147
+
148
+ def transform_class_name(klass_name)
149
+ klass_name.to_s.split(/(?=[A-Z])/).map(&:downcase).join("_")
150
+ end
151
+
152
+ def cache_klass_with_module(klass)
153
+ if RedMatryoshka.configuration.module.present?
154
+ "#{RedMatryoshka.configuration.module}::#{klass}"
155
+ else
156
+ klass
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,13 @@
1
+ module RedMatryoshka
2
+ class Config
3
+ attr_accessor :redis
4
+ attr_accessor :key_prefix
5
+ attr_accessor :module
6
+
7
+ def initialize
8
+ @redis = Redis.new
9
+ @key_prefix = "matryoshka"
10
+ @module = "RedMatryoshka"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,121 @@
1
+ module RedMatryoshka
2
+ class Doll
3
+ KEY_SEPARATOR = ":".freeze
4
+
5
+ def initialize(hash, cache_klass = nil)
6
+ @hash = hash
7
+ @cache_klass = cache_klass
8
+ end
9
+
10
+ def expand
11
+ @hash.inject({}) do |hash, (k, v)|
12
+ row = expand_row k, v
13
+
14
+ if array_with_hashes?(row)
15
+ array_deep_merge hash, row
16
+ else
17
+ hash.deep_merge row
18
+ end
19
+ end
20
+ end
21
+
22
+ def flatten
23
+ @hash.inject({}) { |flatten_hash, (k, v)| flatten_hash.merge flatten_row([], k, v) }
24
+ end
25
+
26
+ private
27
+
28
+ def expand_row(key, value)
29
+ return { key.to_sym => expand_value(value) } unless key.to_s.include? KEY_SEPARATOR
30
+
31
+ split_keys = key.split KEY_SEPARATOR
32
+ key = split_keys.shift
33
+ id_hash = {}
34
+ transform_klass = RedMatryoshka::Base.transformed_class(key)
35
+
36
+ if dependent_klass?(transform_klass)
37
+ key = row_key(transform_klass)
38
+ id_hash = { id: split_keys.shift }
39
+ end
40
+
41
+ expended_row = id_hash.merge expand_row(split_keys.join(KEY_SEPARATOR), value)
42
+ expended_row = [expended_row] if expend_in_array? transform_klass
43
+
44
+ { key.to_sym => expended_row }
45
+ end
46
+
47
+ def flatten_row(flat_key, key, value)
48
+ new_flat_key = flat_key + [key]
49
+
50
+ if value.is_a? Hash
51
+ value.inject({}) { |hash, (k, v)| hash.merge flatten_row(new_flat_key, k, v) }
52
+ elsif value.is_a?(Array) && !value.any?
53
+ {} # In some case, an array expecting ids, could be empty
54
+ elsif value.is_a?(Array) && value.any? && value[0].is_a?(Hash)
55
+ value.inject({}) do |hashes, hash|
56
+ hash.each do |k, v|
57
+ hashes.merge! flatten_row(new_flat_key + [hash[:id].to_s], k, v)
58
+ end
59
+ hashes
60
+ end
61
+ else
62
+ { new_flat_key.join(KEY_SEPARATOR) => value.to_s }
63
+ end
64
+ end
65
+
66
+ def array_deep_merge(main_hash, new_row)
67
+ # Validate if the new row if part of the main hash
68
+ key = new_row.keys.first
69
+ id = new_row.values.flatten.first[:id]
70
+
71
+ if main_hash[key].present?
72
+ # Validate if hash with id already in hash
73
+ # It will be useful for the deep merge
74
+ array_hash_index = main_hash[key].find_index { |hash| hash[:id] == id }
75
+ end
76
+
77
+ new_row[key].each do |hash|
78
+ if array_hash_index.present?
79
+ main_hash[key][array_hash_index].deep_merge! hash
80
+ else
81
+ main_hash[key] = [] if main_hash[key].nil?
82
+ main_hash[key] << hash
83
+ end
84
+ end
85
+
86
+ main_hash
87
+ end
88
+
89
+ def array_with_hashes?(row)
90
+ row.values[0].is_a?(Array) && row.values[0][0].is_a?(Hash)
91
+ end
92
+
93
+ def expend_in_array?(transform_klass)
94
+ @cache_klass.classes_to_depends_on.keys.include?(transform_klass) &&
95
+ @cache_klass.classes_to_depends_on[transform_klass][:as] == :array
96
+ end
97
+
98
+ def row_key(transform_klass)
99
+ @cache_klass.classes_to_depends_on[transform_klass][:for] ||
100
+ cache_klass_with_module(transform_klass).constantize.class_to_fetch_from.downcase
101
+ end
102
+
103
+ def dependent_klass?(transform_klass)
104
+ !@cache_klass.classes_to_depends_on.nil? &&
105
+ @cache_klass.classes_to_depends_on.include?(transform_klass)
106
+ end
107
+
108
+ def expand_value(value)
109
+ return value if value.is_a?(Hash) || value.is_a?(Array)
110
+ value.to_s
111
+ end
112
+
113
+ def cache_klass_with_module(klass)
114
+ if RedMatryoshka.configuration.module.present?
115
+ "#{RedMatryoshka.configuration.module}::#{klass}"
116
+ else
117
+ klass
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,172 @@
1
+ require "active_support/concern"
2
+
3
+ module RedMatryoshka
4
+ module RelatedCache
5
+ extend ::ActiveSupport::Concern
6
+
7
+ included do
8
+ after_save :update_cache
9
+ after_save :update_dependent_cache
10
+
11
+ after_destroy :remove_cache
12
+ after_destroy :remove_dependent_cache
13
+ end
14
+
15
+ def update_cache
16
+ retrieve_cache_klass(self).each do |klass|
17
+ cache_klass = klass.new
18
+ listener_options = cache_klass.classes_to_listen_for[self.class.name]
19
+
20
+ cache_klass.set_sub_id(send(listener_options[:sub])) unless listener_options[:sub].nil?
21
+
22
+ RedMatryoshka::Cache.new(cache_klass).cache(record(klass).reload)
23
+
24
+ update_sub_cache(klass)
25
+ end
26
+ end
27
+
28
+ def update_dependent_cache
29
+ retrieve_cache_klass(self).each do |klass|
30
+ instantiate_klass = klass.new
31
+ hash = main_hash_for_dependence(klass)
32
+
33
+ dependencies(instantiate_klass).each do |dependence|
34
+ keys = keys_to_update(klass, dependence)
35
+ redis.multi do
36
+ keys.each do |key|
37
+ redis.mapped_hmset(key, hash)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ def remove_cache
45
+ retrieve_cache_klass(self).each do |klass|
46
+ cache_klass = klass.new
47
+ listener_options = cache_klass.classes_to_listen_for[self.class.name]
48
+
49
+ cache_klass.set_sub_id(send(listener_options[:sub])) unless listener_options[:sub].nil?
50
+
51
+ RedMatryoshka::Cache.new(cache_klass).delete(record(klass).id)
52
+
53
+ remove_sub_cache(klass)
54
+ end
55
+ end
56
+
57
+ def remove_dependent_cache
58
+ retrieve_cache_klass(self).each do |klass|
59
+ instantiate_klass = klass.new
60
+
61
+ dependencies(instantiate_klass).each do |dependence|
62
+ keys = keys_to_update(klass, dependence)
63
+ redis.multi do
64
+ keys.each do |key|
65
+ redis.del key
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def update_sub_cache(klass)
75
+ cache_klass = klass.new
76
+ hash = RedMatryoshka::Cache.new(cache_klass).cache_doll(record(klass)).flatten
77
+
78
+ # TODO: Use LUA script instead
79
+ sub_keys_to_update(klass).each do |key|
80
+ redis.multi do
81
+ redis.mapped_hmset(key, hash)
82
+ end
83
+ end
84
+ end
85
+
86
+ def remove_sub_cache(klass)
87
+ cache_klass = klass.new
88
+
89
+ # TODO: Use LUA script instead
90
+ sub_keys_to_update(klass).each do |key|
91
+ redis.multi do
92
+ redis.del key
93
+ end
94
+ end
95
+ end
96
+
97
+ def retrieve_cache_klass(record)
98
+ RedMatryoshka::Base.descendants.inject([]) do |descendants, d|
99
+ descendants << d if d.new.classes_to_listen_for.keys.include? record.class.name
100
+ descendants
101
+ end
102
+ end
103
+
104
+ def dependencies(cache_klass)
105
+ cache_klass.classes_dependencies_of.map { |klass| cache_klass_with_module(klass) }
106
+ end
107
+
108
+ def cache_klass_with_module(klass)
109
+ if RedMatryoshka.configuration.module.present?
110
+ "#{RedMatryoshka.configuration.module}::#{klass}".constantize
111
+ else
112
+ klass.constantize
113
+ end
114
+ rescue NameError
115
+ if klass.is_a? String
116
+ klass.constantize
117
+ else
118
+ klass
119
+ end
120
+ end
121
+
122
+ def main_hash_for_dependence(klass)
123
+ cache_klass = klass.new
124
+ listener_options = cache_klass.classes_to_listen_for[self.class.name]
125
+
126
+ cache_klass.set_sub_id(send(listener_options[:sub])) unless listener_options[:sub].nil?
127
+
128
+ hash = RedMatryoshka::Cache.new(cache_klass).cache_doll(record(klass).reload).flatten
129
+ id = record(klass).id
130
+
131
+ hash.inject({}) do |cache_hash, (k, v)|
132
+ new_key = klass.new.cache_key("#{id}:#{k}", false)
133
+ cache_hash.merge(new_key => v)
134
+ end
135
+ end
136
+
137
+ def redis
138
+ @redis ||= RedMatryoshka.configuration.redis
139
+ end
140
+
141
+ def lua_script
142
+ File.open(
143
+ File.dirname(__dir__) + "/utils/hassociate.lua",
144
+ "rb"
145
+ ).read
146
+ end
147
+
148
+ def keys_to_update(klass, dependence)
149
+ redis.eval(
150
+ lua_script,
151
+ [
152
+ cache_klass_with_module(dependence).new.cache_key("*"),
153
+ klass.new.cache_key("#{id}:id", false)
154
+ ]
155
+ )
156
+ end
157
+
158
+ def sub_keys_to_update(klass)
159
+ redis.keys(klass.new.cache_key("*:#{id}"))
160
+ end
161
+
162
+ def record(klass)
163
+ listener_options = klass.new.classes_to_listen_for[self.class.name]
164
+
165
+ if listener_options[:record] != :self
166
+ send(listener_options[:record])
167
+ else
168
+ self
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,3 @@
1
+ module RedMatryoshka
2
+ VERSION = "0.1.0".freeze
3
+ end
@@ -0,0 +1,10 @@
1
+ local keys_to_update = {}
2
+ local all_sub_keys = {}
3
+
4
+ for i, v in ipairs(redis.call('keys', KEYS[1])) do
5
+ if redis.call('hexists', v, KEYS[2]) == 1 then
6
+ table.insert(keys_to_update, v)
7
+ end
8
+ end
9
+
10
+ return keys_to_update
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'red_matryoshka/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "red_matryoshka"
8
+ spec.version = RedMatryoshka::VERSION
9
+ spec.authors = ["Charles Lalonde"]
10
+ spec.email = ["charles@unsplash.com"]
11
+
12
+ spec.summary = %q{Russian dolling cache style in redis}
13
+ spec.homepage = "https://github.com/unsplash/red_matryoshka"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.required_ruby_version = ">= 2.1.5"
22
+
23
+ spec.add_dependency "redis", "~>3.2"
24
+
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ spec.add_development_dependency "mock_redis", "~> 0.16.1"
28
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: red_matryoshka
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Charles Lalonde
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-06-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mock_redis
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.16.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.16.1
69
+ description:
70
+ email:
71
+ - charles@unsplash.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".rubocop.yml"
79
+ - ".ruby-style.yml"
80
+ - ".travis.yml"
81
+ - CODE_OF_CONDUCT.md
82
+ - Gemfile
83
+ - LICENSE.txt
84
+ - README.md
85
+ - Rakefile
86
+ - bin/console
87
+ - bin/setup
88
+ - lib/red_matryoshka.rb
89
+ - lib/red_matryoshka/base.rb
90
+ - lib/red_matryoshka/cache.rb
91
+ - lib/red_matryoshka/config.rb
92
+ - lib/red_matryoshka/doll.rb
93
+ - lib/red_matryoshka/related_cache.rb
94
+ - lib/red_matryoshka/version.rb
95
+ - lib/utils/hassociate.lua
96
+ - red_matryoshka.gemspec
97
+ homepage: https://github.com/unsplash/red_matryoshka
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 2.1.5
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.5.1
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: Russian dolling cache style in redis
121
+ test_files: []