red_matryoshka 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,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: []