├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── lib ├── mongoid-cached-json.rb └── mongoid-cached-json │ ├── array.rb │ ├── cached_json.rb │ ├── config.rb │ ├── hash.rb │ ├── key_references.rb │ ├── mongoid_criteria.rb │ └── version.rb ├── mongoid-cached-json.gemspec └── spec ├── array_spec.rb ├── benchmark_spec.rb ├── cached_json_spec.rb ├── config_spec.rb ├── dalli_spec.rb ├── hash_spec.rb ├── mongoid_criteria_spec.rb ├── spec_helper.rb └── support ├── awesome_artwork.rb ├── awesome_image.rb ├── fast_json_artwork.rb ├── fast_json_image.rb ├── fast_json_url.rb ├── json_embedded_foobar.rb ├── json_employee.rb ├── json_foobar.rb ├── json_manager.rb ├── json_math.rb ├── json_parent_foobar.rb ├── json_polymorphic_embedded_foobar.rb ├── json_polymorphic_referenced_foobar.rb ├── json_referenced_foobar.rb ├── json_supervisor.rb ├── json_transform.rb ├── matchers └── invalidate.rb ├── person.rb ├── poly_company.rb ├── poly_person.rb ├── poly_post.rb ├── prison_cell.rb ├── prison_inmate.rb ├── secret_parent.rb ├── sometimes_secret.rb ├── tool.rb └── tool_box.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # don't commit Gemfile.lock 2 | Gemfile.lock 3 | 4 | # rvm 5 | .rvmrc 6 | 7 | # rdoc generated 8 | rdoc 9 | 10 | # yard generated 11 | doc 12 | .yardoc 13 | 14 | # bundler 15 | .bundle 16 | 17 | # jeweler generated 18 | pkg 19 | 20 | # redcar 21 | .redcar 22 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format=documentation 3 | 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - vendor/**/* 4 | - bin/**/* 5 | 6 | inherit_from: .rubocop_todo.yml 7 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2019-05-30 22:27:20 -0400 using RuboCop version 0.71.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 5 10 | # Configuration parameters: Include. 11 | # Include: **/*.gemfile, **/Gemfile, **/gems.rb 12 | Bundler/DuplicatedGem: 13 | Exclude: 14 | - 'Gemfile' 15 | 16 | # Offense count: 3 17 | # Cop supports --auto-correct. 18 | Layout/EmptyLineAfterGuardClause: 19 | Exclude: 20 | - 'lib/mongoid-cached-json/cached_json.rb' 21 | 22 | # Offense count: 4 23 | # Cop supports --auto-correct. 24 | Layout/EmptyLineAfterMagicComment: 25 | Exclude: 26 | - 'lib/mongoid-cached-json.rb' 27 | - 'lib/mongoid-cached-json/cached_json.rb' 28 | - 'lib/mongoid-cached-json/config.rb' 29 | - 'lib/mongoid-cached-json/key_references.rb' 30 | 31 | # Offense count: 16 32 | # Cop supports --auto-correct. 33 | # Configuration parameters: IndentationWidth. 34 | # SupportedStyles: special_inside_parentheses, consistent, align_brackets 35 | Layout/IndentFirstArrayElement: 36 | EnforcedStyle: consistent 37 | 38 | # Offense count: 1 39 | # Cop supports --auto-correct. 40 | # Configuration parameters: EnforcedStyle. 41 | # SupportedStyles: symmetrical, new_line, same_line 42 | Layout/MultilineArrayBraceLayout: 43 | Exclude: 44 | - 'spec/cached_json_spec.rb' 45 | 46 | # Offense count: 4 47 | # Cop supports --auto-correct. 48 | # Configuration parameters: EnforcedStyle. 49 | # SupportedStyles: symmetrical, new_line, same_line 50 | Layout/MultilineHashBraceLayout: 51 | Exclude: 52 | - 'spec/cached_json_spec.rb' 53 | 54 | # Offense count: 2 55 | # Cop supports --auto-correct. 56 | # Configuration parameters: EnforcedStyle. 57 | # SupportedStyles: symmetrical, new_line, same_line 58 | Layout/MultilineMethodCallBraceLayout: 59 | Exclude: 60 | - 'spec/cached_json_spec.rb' 61 | 62 | # Offense count: 1 63 | # Configuration parameters: AllowSafeAssignment. 64 | Lint/AssignmentInCondition: 65 | Exclude: 66 | - 'lib/mongoid-cached-json/cached_json.rb' 67 | 68 | # Offense count: 1 69 | # Configuration parameters: AllowKeywordBlockArguments. 70 | Lint/UnderscorePrefixedVariableName: 71 | Exclude: 72 | - 'lib/mongoid-cached-json/cached_json.rb' 73 | 74 | # Offense count: 2 75 | Lint/UselessAssignment: 76 | Exclude: 77 | - 'lib/mongoid-cached-json/cached_json.rb' 78 | - 'spec/cached_json_spec.rb' 79 | 80 | # Offense count: 5 81 | Metrics/AbcSize: 82 | Max: 45 83 | 84 | # Offense count: 22 85 | # Configuration parameters: CountComments, ExcludedMethods. 86 | # ExcludedMethods: refine 87 | Metrics/BlockLength: 88 | Max: 488 89 | 90 | # Offense count: 2 91 | # Configuration parameters: CountBlocks. 92 | Metrics/BlockNesting: 93 | Max: 4 94 | 95 | # Offense count: 4 96 | Metrics/CyclomaticComplexity: 97 | Max: 15 98 | 99 | # Offense count: 9 100 | # Configuration parameters: CountComments, ExcludedMethods. 101 | Metrics/MethodLength: 102 | Max: 41 103 | 104 | # Offense count: 1 105 | # Configuration parameters: CountComments. 106 | Metrics/ModuleLength: 107 | Max: 124 108 | 109 | # Offense count: 4 110 | Metrics/PerceivedComplexity: 111 | Max: 16 112 | 113 | # Offense count: 1 114 | # Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms. 115 | # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS 116 | Naming/FileName: 117 | Exclude: 118 | - 'lib/mongoid-cached-json.rb' 119 | 120 | # Offense count: 1 121 | # Cop supports --auto-correct. 122 | # Configuration parameters: EnforcedStyle. 123 | # SupportedStyles: prefer_alias, prefer_alias_method 124 | Style/Alias: 125 | Exclude: 126 | - 'lib/mongoid-cached-json/cached_json.rb' 127 | 128 | # Offense count: 1 129 | # Cop supports --auto-correct. 130 | # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. 131 | # SupportedStyles: assign_to_condition, assign_inside_condition 132 | Style/ConditionalAssignment: 133 | Exclude: 134 | - 'lib/mongoid-cached-json/cached_json.rb' 135 | 136 | # Offense count: 7 137 | Style/Documentation: 138 | Exclude: 139 | - 'spec/**/*' 140 | - 'test/**/*' 141 | - 'lib/mongoid-cached-json/array.rb' 142 | - 'lib/mongoid-cached-json/cached_json.rb' 143 | - 'lib/mongoid-cached-json/config.rb' 144 | - 'lib/mongoid-cached-json/hash.rb' 145 | - 'lib/mongoid-cached-json/key_references.rb' 146 | - 'lib/mongoid-cached-json/mongoid_criteria.rb' 147 | 148 | # Offense count: 2 149 | Style/DoubleNegation: 150 | Exclude: 151 | - 'lib/mongoid-cached-json/cached_json.rb' 152 | 153 | # Offense count: 1 154 | # Cop supports --auto-correct. 155 | Style/EachWithObject: 156 | Exclude: 157 | - 'lib/mongoid-cached-json/hash.rb' 158 | 159 | # Offense count: 3 160 | # Cop supports --auto-correct. 161 | Style/Encoding: 162 | Exclude: 163 | - 'lib/mongoid-cached-json.rb' 164 | - 'lib/mongoid-cached-json/cached_json.rb' 165 | - 'lib/mongoid-cached-json/config.rb' 166 | 167 | # Offense count: 1 168 | Style/EvalWithLocation: 169 | Exclude: 170 | - 'lib/mongoid-cached-json/config.rb' 171 | 172 | # Offense count: 46 173 | # Cop supports --auto-correct. 174 | # Configuration parameters: EnforcedStyle. 175 | # SupportedStyles: always, never 176 | Style/FrozenStringLiteralComment: 177 | Enabled: false 178 | 179 | # Offense count: 3 180 | # Cop supports --auto-correct. 181 | Style/IfUnlessModifier: 182 | Exclude: 183 | - 'lib/mongoid-cached-json/cached_json.rb' 184 | - 'spec/cached_json_spec.rb' 185 | 186 | # Offense count: 5 187 | # Cop supports --auto-correct. 188 | # Configuration parameters: EnforcedStyle. 189 | # SupportedStyles: line_count_dependent, lambda, literal 190 | Style/Lambda: 191 | Exclude: 192 | - 'lib/mongoid-cached-json/cached_json.rb' 193 | - 'spec/support/json_foobar.rb' 194 | - 'spec/support/poly_company.rb' 195 | - 'spec/support/poly_person.rb' 196 | - 'spec/support/sometimes_secret.rb' 197 | 198 | # Offense count: 1 199 | # Cop supports --auto-correct. 200 | # Configuration parameters: EnforcedStyle, Autocorrect. 201 | # SupportedStyles: module_function, extend_self 202 | Style/ModuleFunction: 203 | Exclude: 204 | - 'lib/mongoid-cached-json/config.rb' 205 | 206 | # Offense count: 1 207 | # Cop supports --auto-correct. 208 | # Configuration parameters: EnforcedStyle. 209 | # SupportedStyles: literals, strict 210 | Style/MutableConstant: 211 | Exclude: 212 | - 'lib/mongoid-cached-json/version.rb' 213 | 214 | # Offense count: 2 215 | # Cop supports --auto-correct. 216 | Style/OrAssignment: 217 | Exclude: 218 | - 'lib/mongoid-cached-json/cached_json.rb' 219 | 220 | # Offense count: 2 221 | # Cop supports --auto-correct. 222 | # Configuration parameters: PreferredDelimiters. 223 | Style/PercentLiteralDelimiters: 224 | Exclude: 225 | - 'spec/array_spec.rb' 226 | 227 | # Offense count: 1 228 | # Cop supports --auto-correct. 229 | # Configuration parameters: EnforcedStyle. 230 | # SupportedStyles: compact, exploded 231 | Style/RaiseArgs: 232 | Exclude: 233 | - 'lib/mongoid-cached-json/cached_json.rb' 234 | 235 | # Offense count: 1 236 | # Cop supports --auto-correct. 237 | # Configuration parameters: ConvertCodeThatCanStartToReturnNil, Whitelist. 238 | # Whitelist: present?, blank?, presence, try, try! 239 | Style/SafeNavigation: 240 | Exclude: 241 | - 'lib/mongoid-cached-json/key_references.rb' 242 | 243 | # Offense count: 2 244 | # Cop supports --auto-correct. 245 | # Configuration parameters: EnforcedStyle. 246 | # SupportedStyles: only_raise, only_fail, semantic 247 | Style/SignalException: 248 | Exclude: 249 | - 'lib/mongoid-cached-json/cached_json.rb' 250 | - 'spec/cached_json_spec.rb' 251 | 252 | # Offense count: 13 253 | # Cop supports --auto-correct. 254 | # Configuration parameters: MinSize. 255 | # SupportedStyles: percent, brackets 256 | Style/SymbolArray: 257 | EnforcedStyle: brackets 258 | 259 | # Offense count: 1 260 | # Cop supports --auto-correct. 261 | # Configuration parameters: EnforcedStyle, AllowSafeAssignment. 262 | # SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex 263 | Style/TernaryParentheses: 264 | Exclude: 265 | - 'lib/mongoid-cached-json/cached_json.rb' 266 | 267 | # Offense count: 1 268 | # Cop supports --auto-correct. 269 | Style/UnneededInterpolation: 270 | Exclude: 271 | - 'spec/cached_json_spec.rb' 272 | 273 | # Offense count: 200 274 | # Cop supports --auto-correct. 275 | # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 276 | # URISchemes: http, https 277 | Metrics/LineLength: 278 | Max: 202 279 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | script: bundle exec rake spec 4 | 5 | services: 6 | - mongodb 7 | - memcache 8 | 9 | rvm: 10 | - 2.0.0 11 | - 2.1.10 12 | - 2.2.10 13 | - 2.3.7 14 | - 2.4.6 15 | - 2.5.5 16 | - 2.6.3 17 | 18 | env: 19 | - MONGOID_VERSION=3 20 | - MONGOID_VERSION=4 21 | - MONGOID_VERSION=5 22 | - MONGOID_VERSION=6 23 | - MONGOID_VERSION=7 24 | 25 | cache: bundler 26 | 27 | matrix: 28 | include: 29 | - rvm: 2.6.3 30 | env: RUBOCOP=true 31 | script: bundle exec rake rubocop 32 | exclude: 33 | - rvm: 2.0.0 34 | env: MONGOID_VERSION=6 35 | - rvm: 2.1.10 36 | env: MONGOID_VERSION=6 37 | - rvm: 2.0.0 38 | env: MONGOID_VERSION=7 39 | - rvm: 2.1.10 40 | env: MONGOID_VERSION=7 41 | - rvm: 2.2.10 42 | env: MONGOID_VERSION=7 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.6.1 (Next) 2 | ------------ 3 | 4 | * Your contribution here. 5 | 6 | 1.6.0 (2019/05/31) 7 | ------------ 8 | 9 | * [#20](https://github.com/mongoid/mongoid-cached-json/pull/20): Compatibility with Ruby 2.3, 2.4, 2.5 and 2.6 - [@yuki24](http://github.com/yuki24). 10 | * [#20](https://github.com/mongoid/mongoid-cached-json/pull/20): Compatibility with Mongoid 6 - [@yuki24](http://github.com/yuki24). 11 | * [#20](https://github.com/mongoid/mongoid-cached-json/pull/20): No longer test against Ruby 1.9.3 - [@yuki24](http://github.com/yuki24). 12 | * [#22](https://github.com/mongoid/mongoid-cached-json/pull/22): Compatibility with Mongoid 7 - [@yuki24](https://github.com/yuki24) 13 | 14 | 1.5.3 (2015/09/17) 15 | ------------------ 16 | 17 | * Compatibility with Mongoid 5 - [@dblock](http://github.com/dblock). 18 | 19 | 1.5.2 (2014/29/12) 20 | ------------------ 21 | 22 | * Fixed support for Ruby 2.2.0 - [@dblock](http://github.com/dblock). 23 | * Implemented RuboCop, Ruby-style linter - [@dblock](http://github.com/dblock). 24 | * Upgraded to RSpec 3.1 - [@dblock](http://github.com/dblock). 25 | * Removed Jeweler - [@dblock](http://github.com/dblock). 26 | 27 | 1.5.1 (2013/05/07) 28 | -------------------- 29 | 30 | * Fixed `read_multi` calls so as to enable proper cache reading behavior in stores other than `ActiveSupport::Cache::DalliStore` - [@macreery](http://github.com/macreery). 31 | 32 | 1.5 (2013/04/13) 33 | ---------------- 34 | 35 | * Added `:reference_properties` that disables dynamic selection of the type of JSON to return for a reference - [@dblock](https://github.com/dblock). 36 | 37 | 1.4.3 (2013/01/25) 38 | ------------------ 39 | 40 | * For caches that support `read_multi`, do not attempt to fetch JSON a second time via `fetch`, write it directly to cache - [@dblock](https://github.com/dblock). 41 | 42 | 1.4.2 (2013/01/24) 43 | ------------------ 44 | 45 | * Fix: calling `as_json` on a destroyed Mongoid 3.1 object with a HABTM relationship raises `undefined method 'map' for nil:NilClass` - [@dblock](http://github.com/dblock). 46 | 47 | 1.4.1 (2013/01/22) 48 | ------------------ 49 | 50 | * Invalidate cache in `after_destroy` - [@dblock](http://github.com/dblock). 51 | * Do not invalidate cache when the document is created - [@dblock](http://github.com/dblock). 52 | * Invalidate cache in `after_update` instead of `before_update` - [@dblock](http://github.com/dblock). 53 | 54 | 1.4 (2013/01/20) 55 | --------------- 56 | 57 | * Collect a JSON partial representation first, then fetch data from cache only once per-key - [@dblock](http://github.com/dblock), [@macreery](http://github.com/macreery). 58 | * Use `read_multi` if the cache store supports it to fetch data from cache in bulk - [@dblock](http://github.com/dblock), [@macreery](http://github.com/macreery). 59 | * Added a benchmark test suite - [@dblock](http://github.com/dblock), [@macreery](http://github.com/macreery). 60 | 61 | 1.3 (2012/11/12) 62 | ---------------- 63 | 64 | * Removed requirement for `bson_ext`, support for Mongoid 3.0 - [@dblock](http://github.com/dblock). 65 | 66 | 1.2.3 (2012/07/03) 67 | ------------------ 68 | 69 | * Fix: including a `referenced_in` field in `json_fields` within a child `embedded_in` a parent causes an "access to the collection is not allowed since it is an embedded document" error - [@dblock](http://github.com/dblock). 70 | 71 | 1.2.2 (2012/07/03) 72 | ------------------ 73 | 74 | * Fix [#6](https://github.com/mongoid/mongoid-cached-json/issues/6): including parent in `json_fields` within a polymorphic reference fails with an "uninitialized constant" error - [@dblock](http://github.com/dblock). 75 | 76 | 1.2.1 (2012/06/12) 77 | ------------------ 78 | 79 | * Allow `nil` parameter in as_json - [@dblock](http://github.com/dblock). 80 | 81 | 1.2.0 (2012/05/28) 82 | ------------------ 83 | 84 | * Fix: cache key generation bug when using Mongoid 3 - [@marbemac](http://github.com/marbemac). 85 | 86 | 1.1.1 (2012/03/21) 87 | ------------------ 88 | 89 | * Fix: caching/invalidating referenced polymorphic documents - [@macreery](http://github.com/macreery). 90 | 91 | 1.1 (2012/02/29) 92 | ---------------- 93 | 94 | * Added support for versioning - [@dblock](http://github.com/dblock). 95 | 96 | 1.0 (2012/02/20) 97 | ---------------- 98 | 99 | * Initial release - [@aaw](http://github.com/aaw). 100 | * Retired support for `:markdown` in favor of `Mongoid::CachedJson::transform` - [@dblock](http://github.com/dblock). 101 | * Added `Mongoid::CachedJson::configure` - [@dblock](http://github.com/dblock). 102 | * Added support for `:markdown` - [@macreery](http://github.com/macreery). 103 | 104 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to Mongoid::CachedJson 2 | =================================== 3 | 4 | Mongoid::CachedJson is work of [many of contributors](https://github.com/mongoid/mongoid-cached-json/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/mongoid/mongoid-cached-json/pulls), [propose features and discuss issues](https://github.com/mongoid/mongoid-cached-json/issues). When in doubt, ask a question in the [Google Group](http://groups.google.com/group/mongoid-cached-json). 5 | 6 | #### Fork the Project 7 | 8 | Fork the [project on Github](https://github.com/mongoid/mongoid-cached-json) and check out your copy. 9 | 10 | ``` 11 | git clone https://github.com/contributor/mongoid-cached-json.git 12 | cd mongoid-cached-json 13 | git remote add upstream https://github.com/mongoid/mongoid-cached-json.git 14 | ``` 15 | 16 | #### Create a Topic Branch 17 | 18 | Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. 19 | 20 | ``` 21 | git checkout master 22 | git pull upstream master 23 | git checkout -b my-feature-branch 24 | ``` 25 | 26 | #### Install Memcached and MongoDB 27 | 28 | MongoDB and Memcached are required, use your favorite OS installer. 29 | 30 | ``` 31 | brew install mongodb 32 | brew install memcached 33 | ``` 34 | 35 | #### Bundle Install and Test 36 | 37 | Ensure that you can build the project and run tests. 38 | 39 | ``` 40 | bundle install 41 | bundle exec rake 42 | ``` 43 | 44 | #### Write Tests 45 | 46 | Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. 47 | 48 | We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. 49 | 50 | #### Write Code 51 | 52 | Implement your feature or bug fix. 53 | 54 | Ruby style is enforced with [Rubocop](https://github.com/bbatsov/rubocop), run `bundle exec rubocop` and fix any style issues highlighted. 55 | 56 | Make sure that `bundle exec rake` completes without errors. 57 | 58 | #### Write Documentation 59 | 60 | Document any external behavior in the [README](README.md). 61 | 62 | #### Update Changelog 63 | 64 | Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. Make it look like every other line, including your name and link to your Github account. 65 | 66 | #### Commit Changes 67 | 68 | Make sure git knows your name and email address: 69 | 70 | ``` 71 | git config --global user.name "Your Name" 72 | git config --global user.email "contributor@example.com" 73 | ``` 74 | 75 | Writing good commit logs is important. A commit log should describe what changed and why. 76 | 77 | ``` 78 | git add ... 79 | git commit 80 | ``` 81 | 82 | #### Push 83 | 84 | ``` 85 | git push origin my-feature-branch 86 | ``` 87 | 88 | #### Make a Pull Request 89 | 90 | Go to https://github.com/contributor/mongoid-cached-json and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. 91 | 92 | #### Rebase 93 | 94 | If you've been working on a change for a while, rebase with upstream/master. 95 | 96 | ``` 97 | git fetch upstream 98 | git rebase upstream/master 99 | git push origin my-feature-branch -f 100 | ``` 101 | 102 | #### Update CHANGELOG Again 103 | 104 | Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows. 105 | 106 | ``` 107 | * [#123](https://github.com/mongoid/mongoid-cached-json/pull/123): Reticulated splines - [@contributor](https://github.com/contributor). 108 | ``` 109 | 110 | Amend your previous commit and force push the changes. 111 | 112 | ``` 113 | git commit --amend 114 | git push origin my-feature-branch -f 115 | ``` 116 | 117 | #### Check on Your Pull Request 118 | 119 | Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. 120 | 121 | #### Be Patient 122 | 123 | It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! 124 | 125 | #### Thank You 126 | 127 | Please do know that we really appreciate and value your time and work. We love you, really. 128 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | 5 | case version = ENV['MONGOID_VERSION'] || '~> 5.0' 6 | when /7/ 7 | gem 'mongoid', '~> 7.0' 8 | when /6/ 9 | gem 'mongoid', '~> 6.0' 10 | when /5/ 11 | gem 'mongoid', '~> 5.0' 12 | when /4/ 13 | gem 'mongoid', '~> 4.0' 14 | when /3/ 15 | gem 'mongoid', '~> 3.1' 16 | else 17 | gem 'mongoid', version 18 | end 19 | 20 | group :development do 21 | gem 'dalli', '~> 2.6' 22 | gem 'rake' 23 | gem 'rubocop' 24 | gem 'yard', '~> 0.6' 25 | end 26 | 27 | group :test do 28 | gem 'mongoid-compatibility' 29 | gem 'rspec', '~> 3.1' 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2011 Art.sy, Aaron Windsor, Daniel Doubrovkine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mongoid::CachedJson 2 | =================== 3 | 4 | [![Gem Version](https://badge.fury.io/rb/mongoid-cached-json.svg)](http://badge.fury.io/rb/mongoid-cached-json) 5 | [![Build Status](https://travis-ci.org/mongoid/mongoid-cached-json.svg?branch=master)](https://travis-ci.org/mongoid/mongoid-cached-json) 6 | [![Code Climate](https://codeclimate.com/github/mongoid/mongoid-cached-json.svg)](https://codeclimate.com/github/mongoid/mongoid-cached-json) 7 | 8 | Typical `as_json` definitions may involve lots of database point queries and method calls. When returning collections of objects, a single call may yield hundreds of database queries that can take seconds. This library mitigates the problem by implementing a module called `CachedJson`. 9 | 10 | `CachedJson` enables returning multiple JSON formats and versions from a single class and provides some rules for yielding embedded or referenced data. It then uses a scheme where fragments of JSON are cached for a particular (class, id) pair containing only the data that doesn't involve references/embedded documents. To get the full JSON for an instance, `CachedJson` will combine fragments of JSON from the instance with fragments representing the JSON for its references. In the best case, when all of these fragments are cached, this falls through to a few cache lookups followed by a couple Ruby hash merges to create the JSON. 11 | 12 | Using `Mongoid::CachedJson` we were able to cut our JSON API average response time by about a factor of 10. 13 | 14 | Compatibility 15 | ------------- 16 | 17 | This gem is compatible with Mongoid 3, 4, 5, 6, and 7. 18 | 19 | Resources 20 | --------- 21 | 22 | * [Need Help? Google Group](http://groups.google.com/group/mongoid-cached-json) 23 | * [Source Code](http://github.com/mongoid/mongoid-cached-json) 24 | 25 | Quickstart 26 | ---------- 27 | 28 | Add `mongoid-cached-json` to your Gemfile. 29 | 30 | gem 'mongoid-cached-json' 31 | 32 | Include `Mongoid::CachedJson` in your models. 33 | 34 | ``` ruby 35 | class Gadget 36 | include Mongoid::CachedJson 37 | 38 | field :name 39 | field :extras 40 | 41 | belongs_to :widget 42 | 43 | json_fields \ 44 | name: {}, 45 | extras: { properties: :public } 46 | 47 | end 48 | 49 | class Widget 50 | include Mongoid::CachedJson 51 | 52 | field :name 53 | has_many :gadgets 54 | 55 | json_fields \ 56 | name: {}, 57 | gadgets: { type: :reference, properties: :public } 58 | 59 | end 60 | ``` 61 | 62 | Invoke `as_json`. 63 | 64 | ``` ruby 65 | widget = Widget.first 66 | 67 | # the `:short` version of the JSON, `gadgets` not included 68 | widget.as_json 69 | 70 | # equivalent to the above 71 | widget.as_json(properties: :short) 72 | 73 | # `:public` version of the JSON, `gadgets` returned with `:short` JSON, no `:extras` 74 | widget.as_json(properties: :public) 75 | 76 | # `:all` version of the JSON, `gadgets` returned with `:all` JSON, including `:extras` 77 | widget.as_json(properties: :all) 78 | ``` 79 | 80 | Configuration 81 | ------------- 82 | 83 | By default `Mongoid::CachedJson` will use an instance of `ActiveSupport::Cache::MemoryStore` in a non-Rails and `Rails.cache` in a Rails environment. You can configure it to use any other cache store. 84 | 85 | ``` ruby 86 | Mongoid::CachedJson.configure do |config| 87 | config.cache = ActiveSupport::Cache::FileStore.new 88 | end 89 | ``` 90 | 91 | The default JSON version returned from `as_json` is `:unspecified`. If you wish to redefine this, set `Mongoid::CachedJson.config.default_version`. 92 | 93 | ``` ruby 94 | Mongoid::CachedJson.configure do |config| 95 | config.default_version = :v2 96 | end 97 | ``` 98 | 99 | Defining Fields 100 | --------------- 101 | 102 | `Mongoid::CachedJson` supports the following options: 103 | 104 | * `:hide_as_child_json_when` is an optional function that hides the child JSON from `as_json` parent objects, eg. `cached_json hide_as_child_json_when: lambda { |instance| ! instance.secret? }` 105 | 106 | `Mongoid::CachedJson` field definitions support the following options: 107 | 108 | * `:definition` can be a symbol or an anonymous function, eg. `description: { definition: :name }` or `description: { definition: lambda { |instance| instance.name } }` 109 | * `:type` can be `:reference`, required for referenced objects 110 | * `:properties` can be one of `:short`, `:public`, `:all`, in this order 111 | * `:version` can be a single version for this field to appear in 112 | * `:versions` can be an array of versions for this field to appear in 113 | * `:reference_properties` can be one of `:short`, `:public`, `:all`, default will select the reference properties format dynamically (see below) 114 | 115 | Reference Properties 116 | -------------------- 117 | 118 | When calling `as_json` on a model that contains references to other models the value of the `:properties` option passed into the `as_json` call will be chosen as follows: 119 | 120 | * Use the value of the `:reference_properties` option, if specified. 121 | * For `:short` JSON, use `:short`. 122 | * For `:public` JSON, use `:public`. 123 | * For `:all` JSON, use `:all`. 124 | 125 | The dynamic selection where `:public` generates `:short` references allows to return smaller embedded collections, while `:all` allows to fetch deep data. Another way of looking at this is to say that a field in a `:short` JSON appears in collections, a field declared in the `:public` JSON appears for all users and the field declared in the `:all` JSON appears for object owners only. 126 | 127 | To override this behavior and always return the `:short` JSON for a child reference, use `:reference_properties`. In the following example we would want `Person.as_json(properties: :all)` to return the social security number for that person, but not for all their friends. 128 | 129 | ``` ruby 130 | class Person 131 | include Mongoid::Document 132 | include Mongoid::CachedJson 133 | 134 | field :name 135 | field :ssn 136 | has_and_belongs_to_many :friends, class_name: 'Person' 137 | 138 | json_fields \ 139 | name: {}, 140 | ssn: { properties: :all }, 141 | friends: { properties: :public, reference_properties: :short } 142 | 143 | end 144 | ``` 145 | 146 | Versioning 147 | ---------- 148 | 149 | You can set an optional `version` or `versions` attribute on JSON fields. Consider the following definition where the first version defined `:name`, then split it into `:first`, `:middle` and `:last` in version `:v2` and introduced a date of birth in `:v3`. 150 | 151 | ``` ruby 152 | class Person 153 | include Mongoid::Document 154 | include Mongoid::CachedJson 155 | 156 | field :first 157 | field :last 158 | 159 | def name 160 | [ first, middle, last ].compact.join(' ') 161 | end 162 | 163 | json_fields \ 164 | first: { versions: [ :v2, :v3 ] }, 165 | last: { versions: [ :v2, :v3 ] }, 166 | middle: { versions: [ :v2, :v3 ] }, 167 | born: { versions: :v3 }, 168 | name: { definition: :name } 169 | 170 | end 171 | ``` 172 | 173 | ``` ruby 174 | person = Person.create(first: 'John', middle: 'F.', last: 'Kennedy', born: 'May 29, 1917') 175 | person.as_json # { name: 'John F. Kennedy' } 176 | person.as_json(version: :v2) # { first: 'John', middle: 'F.', last: 'Kennedy', name: 'John F. Kennedy' } 177 | person.as_json(version: :v3) # { first: 'John', middle: 'F.', last: 'Kennedy', name: 'John F. Kennedy', born: 'May 29, 1917' } 178 | ``` 179 | 180 | Transformations 181 | --------------- 182 | 183 | You can define global transformations on all JSON values with `Mongoid::CachedJson.config.transform`. Each transformation must return a value. In the following example we extend the JSON definition with an application-specific `:trusted` field and encode any content that is not trusted. 184 | 185 | ``` ruby 186 | class Widget 187 | include Mongoid::Document 188 | include Mongoid::CachedJson 189 | 190 | field :name 191 | field :description 192 | 193 | json_fields \ 194 | name: { trusted: true }, 195 | description: {} 196 | end 197 | ``` 198 | 199 | ``` ruby 200 | Mongoid::CachedJson.config.transform do |field, definition, value| 201 | trusted = !!definition[:trusted] 202 | trusted ? value : CGI.escapeHTML(value) 203 | end 204 | ``` 205 | 206 | Mixing with Standard as_json 207 | ---------------------------- 208 | 209 | Taking part in the `Mongoid::CachedJson` `json_fields` scheme is optional: you can still write `as_json` methods where it makes sense. 210 | 211 | Turning Caching Off 212 | ------------------- 213 | 214 | You can set `Mongoid::CachedJson.config.disable_caching = true`. It may be a good idea to set it to `ENV['DISABLE_JSON_CACHING']`, in case this turns out not to be The Solution To All Of Your Performance Problems (TM). 215 | 216 | Testing JSON 217 | ------------ 218 | 219 | This library overrides `as_json`, hence testing JSON results can be done at model level. 220 | 221 | ``` ruby 222 | describe 'as_json' do 223 | before :each do 224 | @person = Person.create!(first: 'John', last: 'Doe') 225 | end 226 | it 'returns name' do 227 | expect(@person.as_json(properties: :public)[:name]).to eql 'John Doe' 228 | end 229 | end 230 | ``` 231 | 232 | It's also common to test the results of the API using the [Pathy](https://github.com/twoism/pathy) library. 233 | 234 | ``` ruby 235 | describe 'as_json' do 236 | before :each do 237 | person = Person.create!(first: 'John', last: 'Doe') 238 | end 239 | it 'returns name' do 240 | get "/api/person/#{person.id}" 241 | expect(response.body.at_json_path('name')).to eql 'John Doe' 242 | end 243 | end 244 | ``` 245 | 246 | Testing Cache Invalidation 247 | -------------------------- 248 | 249 | Cache is invalidated by calling `:expire_cached_json` on an instance. 250 | 251 | ``` ruby 252 | describe 'updating a person' do 253 | before :each 254 | @person = Person.create!(name: 'John Doe') 255 | end 256 | it 'invalidates cache' do 257 | expect(@person).to receive(:expire_cached_json) 258 | @person.update_attributes!(name: 'updated') 259 | end 260 | end 261 | ``` 262 | You may also want to use [this RSpec matcher](spec/support/matchers/invalidate.rb). 263 | 264 | ```ruby 265 | describe 'updating a person' do 266 | it 'invalidates cache' do 267 | expect do 268 | @person.update_attributes!(name: 'updated') 269 | end.to invalidate @person 270 | end 271 | end 272 | ``` 273 | 274 | Performance 275 | ----------- 276 | 277 | This gem implements two interesting optimizations. 278 | 279 | ### Bulk Reference Resolving w/ Local Store 280 | 281 | Consider an array of Mongoid instances, each with numerous references to other objects. It's typical to see such instances reference the same object. `Mongoid::CachedJson` first collects all JSON references, then resolves them after suppressing duplicates. This significantly reduces the number of cache queries. 282 | 283 | ### Fetching Cache Data in Bulk 284 | 285 | Various cache stores, including Memcached, support bulk read operations. The [Dalli](https://github.com/mperham/dalli) gem exposes this via the `read_multi` method. `Mongoid::CachedJson` will always invoke `read_multi` where available, which significantly reduces the number of network roundtrips to the cache servers. 286 | 287 | Contributing 288 | ------------ 289 | 290 | See [CONTRIBUTING](CONTRIBUTING.md). 291 | 292 | Copyright and License 293 | --------------------- 294 | 295 | MIT License, see [LICENSE](https://github.com/mongoid/mongoid-cached-json/blob/master/LICENSE.md) for details. 296 | 297 | (c) 2012-2014 [Artsy](https://artsy.net) and [Contributors](CHANGELOG.md) 298 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | 4 | require File.expand_path('lib/mongoid-cached-json/version', __dir__) 5 | 6 | Bundler.setup(:default, :development) 7 | Bundler::GemHelper.install_tasks 8 | 9 | require 'rake' 10 | 11 | require 'rspec/core' 12 | require 'rspec/core/rake_task' 13 | 14 | RSpec::Core::RakeTask.new(:spec) do |spec| 15 | files = FileList['spec/**/*_spec.rb'] 16 | files = files.exclude 'spec/benchmark_spec.rb' 17 | spec.pattern = files 18 | end 19 | 20 | require 'rdoc/task' 21 | Rake::RDocTask.new do |rdoc| 22 | version = File.exist?('VERSION') ? File.read('VERSION') : '' 23 | 24 | rdoc.rdoc_dir = 'rdoc' 25 | rdoc.title = "mongoid-cached-json #{version}" 26 | rdoc.rdoc_files.include('README*') 27 | rdoc.rdoc_files.include('LICENSE*') 28 | rdoc.rdoc_files.include('lib/**/*.rb') 29 | end 30 | 31 | require 'rubocop/rake_task' 32 | RuboCop::RakeTask.new 33 | 34 | task default: [:rubocop, :spec] 35 | -------------------------------------------------------------------------------- /lib/mongoid-cached-json.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'active_support/concern' 3 | require 'mongoid' 4 | require 'mongoid-cached-json/key_references' 5 | require 'mongoid-cached-json/array' 6 | require 'mongoid-cached-json/hash' 7 | require 'mongoid-cached-json/mongoid_criteria' 8 | require 'mongoid-cached-json/version' 9 | require 'mongoid-cached-json/config' 10 | require 'mongoid-cached-json/cached_json' 11 | -------------------------------------------------------------------------------- /lib/mongoid-cached-json/array.rb: -------------------------------------------------------------------------------- 1 | class Array 2 | def as_json_partial(options = {}) 3 | json_keys = nil 4 | json = map do |i| 5 | if i.respond_to?(:as_json_partial) 6 | partial_json_keys, json = i.as_json_partial(options) 7 | json_keys = json_keys ? json_keys.merge_set(partial_json_keys) : partial_json_keys 8 | json 9 | else 10 | i.as_json(options) 11 | end 12 | end 13 | [json_keys, json] 14 | end 15 | 16 | def as_json(options = {}) 17 | json_keys, json = as_json_partial(options) 18 | Mongoid::CachedJson.materialize_json_references_with_read_multi(json_keys, json) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/mongoid-cached-json/cached_json.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Mongoid 3 | module CachedJson 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | class_attribute :all_json_properties 8 | class_attribute :all_json_versions 9 | class_attribute :cached_json_field_defs 10 | class_attribute :cached_json_reference_defs 11 | class_attribute :hide_as_child_json_when 12 | end 13 | 14 | module ClassMethods 15 | # Define JSON fields for a class. 16 | # 17 | # @param [ hash ] defs JSON field definition. 18 | # 19 | # @since 1.0 20 | def json_fields(defs) 21 | self.hide_as_child_json_when = defs.delete(:hide_as_child_json_when) || lambda { |_a| false } 22 | self.all_json_properties = [:short, :public, :all] 23 | cached_json_defs = Hash[defs.map { |k, v| [k, { type: :callable, properties: :short, definition: k }.merge(v)] }] 24 | self.cached_json_field_defs = {} 25 | self.cached_json_reference_defs = {} 26 | # Collect all versions for clearing cache 27 | self.all_json_versions = cached_json_defs.map do |_field, definition| 28 | [:unspecified, definition[:version], Array(definition[:versions])] 29 | end.flatten.compact.uniq 30 | all_json_properties.each_with_index do |property, i| 31 | cached_json_field_defs[property] = Hash[cached_json_defs.find_all do |_field, definition| 32 | all_json_properties.find_index(definition[:properties]) <= i && definition[:type] == :callable 33 | end] 34 | cached_json_reference_defs[property] = Hash[cached_json_defs.find_all do |_field, definition| 35 | all_json_properties.find_index(definition[:properties]) <= i && definition[:type] == :reference 36 | end] 37 | # If the field is a reference and is just specified as a symbol, reflect on it to get metadata 38 | cached_json_reference_defs[property].to_a.each do |field, definition| 39 | if definition[:definition].is_a?(Symbol) 40 | cached_json_reference_defs[property][field][:metadata] = reflect_on_association(definition[:definition]) 41 | end 42 | end 43 | end 44 | after_update :expire_cached_json 45 | after_destroy :expire_cached_json 46 | end 47 | 48 | # Materialize a cached JSON within a cache block. 49 | def materialize_cached_json(clazz, id, object_reference, options) 50 | is_top_level_json = options[:is_top_level_json] || false 51 | object_reference = clazz.where(_id: id).first unless object_reference 52 | if !object_reference || (!is_top_level_json && options[:properties] != :all && clazz.hide_as_child_json_when.call(object_reference)) 53 | nil 54 | else 55 | Hash[clazz.cached_json_field_defs[options[:properties]].map do |field, definition| 56 | # version match 57 | versions = ([definition[:version]] | Array(definition[:versions])).compact 58 | next unless versions.empty? || versions.include?(options[:version]) 59 | json_value = (definition[:definition].is_a?(Symbol) ? object_reference.send(definition[:definition]) : definition[:definition].call(object_reference)) 60 | Mongoid::CachedJson.config.transform.each do |t| 61 | json_value = t.call(field, definition, json_value) 62 | end 63 | [field, json_value] 64 | end.compact] 65 | end 66 | end 67 | 68 | # Given an object definition in the form of either an object or a class, id pair, 69 | # grab the as_json representation from the cache if possible, otherwise create 70 | # the as_json representation by loading the object from the database. For any 71 | # references in the object's JSON representation, we have to recursively materialize 72 | # the JSON by calling resolve_json_reference on each of them (which may, in turn, 73 | # call materialize_json) 74 | def materialize_json(options, object_def) 75 | return nil if !object_def[:object] && !object_def[:id] 76 | is_top_level_json = options[:is_top_level_json] || false 77 | if object_def[:object] 78 | object_reference = object_def[:object] 79 | clazz = object_def[:object].class 80 | id = object_def[:object].id 81 | else 82 | object_reference = nil 83 | clazz = object_def[:clazz] 84 | id = object_def[:id] 85 | end 86 | key = cached_json_key(options, clazz, id) 87 | json = { _ref: { _clazz: self, _key: key, _materialize_cached_json: [clazz, id, object_reference, options] } } 88 | keys = KeyReferences.new 89 | keys.set_and_add(key, json) 90 | reference_defs = clazz.cached_json_reference_defs[options[:properties]] 91 | unless reference_defs.empty? 92 | object_reference = clazz.where(_id: id).first unless object_reference 93 | if object_reference && (is_top_level_json || options[:properties] == :all || !clazz.hide_as_child_json_when.call(object_reference)) 94 | json.merge!(Hash[reference_defs.map do |field, definition| 95 | json_properties_type = definition[:reference_properties] || ((options[:properties] == :all) ? :all : :short) 96 | reference_keys, reference = clazz.resolve_json_reference(options.merge(properties: json_properties_type, is_top_level_json: false), object_reference, field, definition) 97 | if reference.is_a?(Hash) && ref = reference[:_ref] 98 | ref[:_parent] = json 99 | ref[:_field] = field 100 | end 101 | keys.merge_set(reference_keys) 102 | [field, reference] 103 | end]) 104 | end 105 | end 106 | [keys, json] 107 | end 108 | 109 | # Cache key. 110 | def cached_json_key(options, cached_class, cached_id) 111 | base_class_name = cached_class.collection_name.to_s.singularize.camelize 112 | "as_json/#{options[:version]}/#{base_class_name}/#{cached_id}/#{options[:properties]}/#{!!options[:is_top_level_json]}" 113 | end 114 | 115 | # If the reference is a symbol, we may be lucky and be able to figure out the as_json 116 | # representation by the (class, id) pair definition of the reference. That is, we may 117 | # be able to load the as_json representation from the cache without even getting the 118 | # model from the database and materializing it through Mongoid. We'll try to do this first. 119 | def resolve_json_reference(options, object, _field, reference_def) 120 | keys = nil 121 | reference_json = nil 122 | if reference_def[:metadata] 123 | key = reference_def[:metadata].key.to_sym 124 | if reference_def[:metadata].polymorphic? 125 | clazz = reference_def[:metadata].inverse_class_name.constantize 126 | else 127 | clazz = reference_def[:metadata].class_name.constantize 128 | end 129 | relation_class = if Mongoid::Compatibility::Version.mongoid7_or_newer? 130 | Mongoid::Association::Referenced::HasAndBelongsToMany::Proxy 131 | else 132 | Mongoid::Relations::Referenced::ManyToMany 133 | end 134 | 135 | if reference_def[:metadata].relation == relation_class 136 | object_ids = object.send(key) 137 | if object_ids 138 | reference_json = object_ids.map do |id| 139 | materialize_keys, json = materialize_json(options, clazz: clazz, id: id) 140 | keys = keys ? keys.merge_set(materialize_keys) : materialize_keys 141 | json 142 | end.compact 143 | else 144 | object_ids = [] 145 | end 146 | end 147 | end 148 | # If we get to this point and reference_json is still nil, there's no chance we can 149 | # load the JSON from cache so we go ahead and call as_json on the object. 150 | unless reference_json 151 | reference_def_definition = reference_def[:definition] 152 | reference = reference_def_definition.is_a?(Symbol) ? object.send(reference_def_definition) : reference_def_definition.call(object) 153 | reference_json = nil 154 | if reference 155 | if reference.respond_to?(:as_json_partial) 156 | reference_keys, reference_json = reference.as_json_partial(options) 157 | keys = keys ? keys.merge_set(reference_keys) : reference_keys 158 | else 159 | reference_json = reference.as_json(options) 160 | end 161 | end 162 | end 163 | [keys, reference_json] 164 | end 165 | end 166 | 167 | # Check whether the cache supports :read_multi and prefetch the data if it does. 168 | def self.materialize_json_references_with_read_multi(key_refs, partial_json) 169 | unfrozen_keys = key_refs.keys.to_a.map(&:dup) if key_refs # see https://github.com/mperham/dalli/pull/320 170 | read_multi = unfrozen_keys && Mongoid::CachedJson.config.cache.respond_to?(:read_multi) 171 | local_cache = read_multi ? Mongoid::CachedJson.config.cache.read_multi(*unfrozen_keys) : {} 172 | Mongoid::CachedJson.materialize_json_references(key_refs, local_cache, read_multi) if key_refs 173 | partial_json 174 | end 175 | 176 | # Materialize all the JSON references in place. 177 | def self.materialize_json_references(key_refs, local_cache = {}, read_multi = false) 178 | key_refs.each_pair do |key, refs| 179 | refs.each do |ref| 180 | _ref = ref.delete(:_ref) 181 | key = _ref[:_key] 182 | fetched_json = local_cache[key] if local_cache.key?(key) 183 | unless fetched_json 184 | if read_multi 185 | # no value in cache, materialize and write 186 | fetched_json = (local_cache[key] = _ref[:_clazz].materialize_cached_json(* _ref[:_materialize_cached_json])) 187 | Mongoid::CachedJson.config.cache.write(key, fetched_json) unless Mongoid::CachedJson.config.disable_caching 188 | else 189 | # fetch/write from cache 190 | fetched_json = (local_cache[key] = Mongoid::CachedJson.config.cache.fetch(key, force: !!Mongoid::CachedJson.config.disable_caching) do 191 | _ref[:_clazz].materialize_cached_json(* _ref[:_materialize_cached_json]) 192 | end) 193 | end 194 | end 195 | if fetched_json 196 | ref.merge! fetched_json 197 | elsif _ref[:_parent] 198 | # a single _ref that resolved to a nil 199 | _ref[:_parent][_ref[:_field]] = nil 200 | end 201 | end 202 | end 203 | end 204 | 205 | # Return a partial JSON without resolved references and all the keys. 206 | def as_json_partial(options = {}) 207 | options ||= {} 208 | if options[:properties] && !all_json_properties.member?(options[:properties]) 209 | fail ArgumentError.new("Unknown properties option: #{options[:properties]}") 210 | end 211 | # partial, unmaterialized JSON 212 | keys, partial_json = self.class.materialize_json({ 213 | properties: :short, is_top_level_json: true, version: Mongoid::CachedJson.config.default_version 214 | }.merge(options), object: self) 215 | [keys, partial_json] 216 | end 217 | 218 | # Fetch the partial JSON and materialize all JSON references. 219 | def as_json_cached(options = {}) 220 | keys, json = as_json_partial(options) 221 | Mongoid::CachedJson.materialize_json_references_with_read_multi(keys, json) 222 | end 223 | 224 | # Return the JSON representation of the object. 225 | def as_json(options = {}) 226 | as_json_cached(options) 227 | end 228 | 229 | # Expire all JSON entries for this class. 230 | def expire_cached_json 231 | all_json_properties.each do |properties| 232 | [true, false].each do |is_top_level_json| 233 | all_json_versions.each do |version| 234 | Mongoid::CachedJson.config.cache.delete(self.class.cached_json_key({ 235 | properties: properties, 236 | is_top_level_json: is_top_level_json, 237 | version: version 238 | }, self.class, id)) 239 | end 240 | end 241 | end 242 | end 243 | 244 | class << self 245 | # Set the configuration options. Best used by passing a block. 246 | # 247 | # @example Set up configuration options. 248 | # Mongoid::CachedJson.configure do |config| 249 | # config.cache = Rails.cache 250 | # end 251 | # 252 | # @return [ Config ] The configuration obejct. 253 | def configure 254 | block_given? ? yield(Mongoid::CachedJson::Config) : Mongoid::CachedJson::Config 255 | end 256 | alias_method :config, :configure 257 | end 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /lib/mongoid-cached-json/config.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Mongoid 3 | module CachedJson 4 | module Config 5 | extend self 6 | 7 | # Current configuration settings. 8 | attr_accessor :settings 9 | 10 | # Default configuration settings. 11 | attr_accessor :defaults 12 | 13 | @settings = {} 14 | @defaults = {} 15 | 16 | # Define a configuration option with a default. 17 | # 18 | # @example Define the option. 19 | # Config.option(:cache, :default => nil) 20 | # 21 | # @param [ Symbol ] name The name of the configuration option. 22 | # @param [ Hash ] options Extras for the option. 23 | # 24 | # @option options [ Object ] :default The default value. 25 | def option(name, options = {}) 26 | defaults[name] = settings[name] = options[:default] 27 | 28 | class_eval <<-RUBY 29 | def #{name} 30 | settings[#{name.inspect}] 31 | end 32 | 33 | def #{name}=(value) 34 | settings[#{name.inspect}] = value 35 | end 36 | 37 | def #{name}? 38 | #{name} 39 | end 40 | RUBY 41 | end 42 | 43 | # Disable caching. 44 | option :disable_caching, default: false 45 | 46 | # Returns the default JSON version 47 | # 48 | # @example Get the default JSON version 49 | # config.default_version 50 | # 51 | # @return [ Version ] The default JSON version. 52 | def default_version 53 | settings[:default_version] = :unspecified unless settings.key?(:default_version) 54 | settings[:default_version] 55 | end 56 | 57 | # Sets the default JSON version. 58 | # 59 | # @example Set the default version. 60 | # config.default_version = :v2 61 | # 62 | # @return [ Version ] The newly set default version. 63 | def default_version=(default_version) 64 | settings[:default_version] = default_version 65 | end 66 | 67 | # Returns the default cache store, for example Rails cache or an instance of ActiveSupport::Cache::MemoryStore. 68 | # 69 | # @example Get the default cache store 70 | # config.default_cache 71 | # 72 | # @return [ Cache ] The default Cache instance. 73 | def default_cache 74 | defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : ::ActiveSupport::Cache::MemoryStore.new 75 | end 76 | 77 | # Returns the cache, or defaults to Rails cache when running under Rails or ActiveSupport::Cache::MemoryStore. 78 | # 79 | # @example Get the cache. 80 | # config.cache 81 | # 82 | # @return [ Cache ] The configured cache or a default cache instance. 83 | def cache 84 | settings[:cache] = default_cache unless settings.key?(:cache) 85 | settings[:cache] 86 | end 87 | 88 | # Sets the cache to use. 89 | # 90 | # @example Set the cache. 91 | # config.cache = Rails.cache 92 | # 93 | # @return [ Cache ] The newly set cache. 94 | def cache=(cache) 95 | settings[:cache] = cache 96 | end 97 | 98 | # Reset the configuration options to the defaults. 99 | # 100 | # @example Reset the configuration options. 101 | # config.reset! 102 | def reset! 103 | settings.replace(defaults) 104 | end 105 | 106 | # Define a transformation on JSON data. 107 | # 108 | # @example Convert every string in materialized JSON to upper-case. 109 | # config.transform do |field, value| 110 | # value.upcase 111 | # end 112 | def transform(& block) 113 | settings[:transform] = [] unless settings.key?(:transform) 114 | settings[:transform] << block if block_given? 115 | settings[:transform] 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/mongoid-cached-json/hash.rb: -------------------------------------------------------------------------------- 1 | class Hash 2 | def as_json_partial(options = {}) 3 | json_keys = nil 4 | json = inject({}) do |h, (k, v)| 5 | if v.respond_to?(:as_json_partial) 6 | partial_json_keys, partial_json = v.as_json_partial(options) 7 | json_keys = json_keys ? json_keys.merge_set(partial_json_keys) : partial_json_keys 8 | h[k] = partial_json 9 | else 10 | h[k] = v.as_json(options) 11 | end 12 | h 13 | end 14 | [json_keys, json] 15 | end 16 | 17 | def as_json(options = {}) 18 | json_keys, json = as_json_partial(options) 19 | Mongoid::CachedJson.materialize_json_references_with_read_multi(json_keys, json) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/mongoid-cached-json/key_references.rb: -------------------------------------------------------------------------------- 1 | # Keep key references to be replaced once the entire JSON is available. 2 | # encoding: utf-8 3 | module Mongoid 4 | module CachedJson 5 | class KeyReferences < Hash 6 | def merge_set(keys) 7 | if keys 8 | keys.each_pair do |k, jsons| 9 | self[k] ||= [] 10 | self[k].concat(jsons) 11 | end 12 | end 13 | self 14 | end 15 | 16 | def set_and_add(key, json) 17 | self[key] ||= [] 18 | self[key] << json 19 | self 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/mongoid-cached-json/mongoid_criteria.rb: -------------------------------------------------------------------------------- 1 | module Mongoid 2 | class Criteria 3 | def as_json_partial(options = {}) 4 | json_keys = nil 5 | json = map do |i| 6 | if i.respond_to?(:as_json_partial) 7 | partial_json_keys, partial_json = i.as_json_partial(options) 8 | json_keys = json_keys ? json_keys.merge_set(partial_json_keys) : partial_json_keys 9 | partial_json 10 | else 11 | i.as_json(options) 12 | end 13 | end 14 | [json_keys, json] 15 | end 16 | 17 | def as_json(options = {}) 18 | json_keys, json = as_json_partial(options) 19 | Mongoid::CachedJson.materialize_json_references_with_read_multi(json_keys, json) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/mongoid-cached-json/version.rb: -------------------------------------------------------------------------------- 1 | module Mongoid 2 | module CachedJson 3 | VERSION = '1.6.0' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /mongoid-cached-json.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.push File.expand_path('lib', __dir__) 2 | require 'mongoid-cached-json/version' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'mongoid-cached-json' 6 | s.version = Mongoid::CachedJson::VERSION 7 | s.authors = ['Aaron Windsor', 'Daniel Doubrovkine', 'Frank Macreery'] 8 | s.email = 'dblock@dblock.org' 9 | s.platform = Gem::Platform::RUBY 10 | s.required_rubygems_version = '>= 1.3.6' 11 | s.files = `git ls-files`.split("\n") 12 | s.require_paths = ['lib'] 13 | s.homepage = 'http://github.com/mongoid/mongoid-cached-json' 14 | s.licenses = ['MIT'] 15 | s.summary = 'Cached-json is a DSL for describing JSON representations of Mongoid models.' 16 | s.add_dependency 'mongoid', '>= 3.0' 17 | end 18 | -------------------------------------------------------------------------------- /spec/array_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Array do 4 | it 'array' do 5 | expect([:x, 'y'].as_json).to eq(%w(x y)) 6 | end 7 | it 'materializes multiple objects that may or may not respond to as_json_partial' do 8 | foobar1 = JsonFoobar.create!(foo: 'FOO1', baz: 'BAZ', bar: 'BAR') 9 | foobar2 = JsonFoobar.create!(foo: 'FOO2', baz: 'BAZ', bar: 'BAR') 10 | expect([[:x, :y], foobar1, foobar2, foobar1, { x: foobar1, y: 'z' }].as_json).to eq([ 11 | %w(x y), 12 | { :foo => 'FOO1', 'Baz' => 'BAZ', :default_foo => 'DEFAULT_FOO' }, 13 | { :foo => 'FOO2', 'Baz' => 'BAZ', :default_foo => 'DEFAULT_FOO' }, 14 | { :foo => 'FOO1', 'Baz' => 'BAZ', :default_foo => 'DEFAULT_FOO' }, 15 | { x: { :foo => 'FOO1', 'Baz' => 'BAZ', :default_foo => 'DEFAULT_FOO' }, y: 'z' } 16 | ]) 17 | end 18 | context 'without read_multi' do 19 | before :each do 20 | Mongoid::CachedJson.config.cache.instance_eval { undef :read_multi } 21 | end 22 | it 'uses a local cache to fetch repeated objects' do 23 | tool = Tool.create!(name: 'hammer') 24 | expect(Mongoid::CachedJson.config.cache).to receive(:fetch).once.and_return( 25 | x: :y 26 | ) 27 | expect([tool, tool, tool].as_json(properties: :all)).to eq([ 28 | { tool_box: nil, x: :y }, 29 | { tool_box: nil, x: :y }, 30 | { tool_box: nil, x: :y } 31 | ]) 32 | end 33 | end 34 | context 'with read_multi' do 35 | it 'uses a local cache to fetch repeated objects' do 36 | tool = Tool.create!(name: 'hammer') 37 | tool_key = "as_json/unspecified/Tool/#{tool.id}/all/true" 38 | expect(Mongoid::CachedJson.config.cache).to receive(:read_multi).once.with(tool_key).and_return( 39 | tool_key => { x: :y } 40 | ) 41 | expect([tool, tool, tool].as_json(properties: :all)).to eq([ 42 | { tool_box: nil, x: :y }, 43 | { tool_box: nil, x: :y }, 44 | { tool_box: nil, x: :y } 45 | ]) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/benchmark_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'benchmark' 3 | require 'active_support/cache/dalli_store' 4 | 5 | describe Mongoid::CachedJson do 6 | before(:all) do 7 | @n = 100 8 | puts "Benchmarking #{Mongoid::CachedJson::VERSION} with #{RUBY_DESCRIPTION}, Dalli #{Dalli::VERSION}" 9 | end 10 | 11 | before do 12 | # flat record 13 | @flat = JsonFoobar.create!(foo: SecureRandom.uuid, baz: SecureRandom.uuid, bar: SecureRandom.uuid) 14 | # has_many 15 | @manager = JsonManager.create(name: 'Boss') 16 | @manager.json_employees.create(name: 'Peon') 17 | @manager.json_employees.create(name: 'Indentured servant') 18 | @manager.json_employees.create(name: 'Serf', nickname: 'Vince') 19 | # has_one 20 | @artwork = AwesomeArtwork.create(name: 'Mona Lisa') 21 | @artwork.create_awesome_image(name: 'Picture of Mona Lisa') 22 | # child and parent with secrets 23 | @child_secret = SometimesSecret.create(should_tell_secret: true) 24 | @child_not_secret = SometimesSecret.create(should_tell_secret: false) 25 | @parent_with_secret = SecretParent.create(name: 'Parent') 26 | @parent_with_secret.create_sometimes_secret(should_tell_secret: true) 27 | # habtm 28 | @habtm = FastJsonArtwork.create 29 | @habtm_image = @habtm.create_fast_json_image 30 | @habtm_image.fast_json_urls.create 31 | @habtm_image.fast_json_urls.create 32 | @habtm_image.fast_json_urls.create 33 | # transform 34 | Mongoid::CachedJson.config.transform do |_field, definition, value| 35 | definition[:transform] ? value.send(definition[:transform].to_sym) : value 36 | end 37 | @transform = JsonTransform.create!(upcase: 'upcase', downcase: 'DOWNCASE', nochange: 'eLiTe') 38 | # polymorphic 39 | @embedded = JsonEmbeddedFoobar.new(foo: 'embedded') 40 | @referenced = JsonReferencedFoobar.new(foo: 'referenced') 41 | @poly_parent = JsonParentFoobar.create!( 42 | json_polymorphic_embedded_foobar: @embedded, 43 | json_polymorphic_referenced_foobar: @referenced 44 | ) 45 | @referenced.save! 46 | # polymorphic relationships 47 | @company = PolyCompany.create! 48 | @company_post = PolyPost.create!(postable: @company) 49 | @person = PolyPerson.create! 50 | @person_post = PolyPost.create!(postable: @person) 51 | # embeds_many 52 | @cell = PrisonCell.create!(number: 42) 53 | @cell.inmates.create!(nickname: 'Joe', person: Person.create!(first: 'Joe')) 54 | @cell.inmates.create!(nickname: 'Bob', person: Person.create!(first: 'Bob')) 55 | # belongs_to 56 | @tool_box = ToolBox.create!(color: 'red') 57 | @hammer = Tool.create!(name: 'hammer', tool_box: @tool_box) 58 | @screwdriver = Tool.create!(name: 'screwdriver', tool_box: @tool_box) 59 | @saw = Tool.create!(name: 'saw', tool_box: @tool_box) 60 | end 61 | 62 | [:dalli_store, :memory_store].each do |cache_store| 63 | context cache_store do 64 | before :each do 65 | @cache = Mongoid::CachedJson::Config.cache 66 | Mongoid::CachedJson.configure do |config| 67 | config.cache = ActiveSupport::Cache.lookup_store(cache_store) 68 | config.cache.clear 69 | end 70 | end 71 | after :each do 72 | Mongoid::CachedJson::Config.cache = @cache 73 | end 74 | 75 | it 'benchmark' do 76 | all_times = [] 77 | [ 78 | :flat, :manager, :artwork, 79 | :child_secret, :child_not_secret, :parent_with_secret, 80 | :habtm, :habtm_image, 81 | :transform, 82 | :poly_parent, :embedded, :referenced, 83 | :company, :person, :company_post, :person_post, 84 | :cell, 85 | :tool_box, :hammer, :screwdriver, :saw 86 | ].each do |record| 87 | times = [] 88 | times << Benchmark.realtime do 89 | [:short, :public, :all].each do |properties| 90 | instance = instance_variable_get("@#{record}".to_sym) 91 | expect(instance).not_to be_nil 92 | @n.times do 93 | # instance 94 | json = instance.as_json(properties: properties) 95 | expect(json).not_to be_nil 96 | expect(json).not_to eq({}) 97 | end 98 | # class 99 | if instance.class.respond_to?(:all) 100 | json = instance.class.all.as_json(properties: properties) 101 | expect(json).not_to be_nil 102 | end 103 | end 104 | end 105 | all_times.concat(times) 106 | avg = times.reduce { |sum, time| sum + time } / times.size 107 | puts "#{cache_store}:#{record} => #{avg}" 108 | end 109 | avg = all_times.reduce { |sum, time| sum + time } / all_times.size 110 | puts '=' * 40 111 | puts "#{cache_store} => #{avg}" 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /spec/cached_json_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::CachedJson do 4 | it 'has a version' do 5 | expect(Mongoid::CachedJson::VERSION).not_to be_nil 6 | expect(Mongoid::CachedJson::VERSION.to_f).to be > 0 7 | end 8 | 9 | [:dalli_store, :memory_store].each do |cache_store| 10 | context "#{cache_store}" do 11 | before :each do 12 | @cache = Mongoid::CachedJson::Config.cache 13 | Mongoid::CachedJson.configure do |config| 14 | config.cache = ActiveSupport::Cache.lookup_store(cache_store) 15 | end 16 | if cache_store == :memory_store && Mongoid::CachedJson.config.cache.respond_to?(:read_multi) 17 | Mongoid::CachedJson.config.cache.instance_eval { undef :read_multi } 18 | end 19 | end 20 | after :each do 21 | Mongoid::CachedJson::Config.cache = @cache 22 | end 23 | context 'with basic fields defined for export with json_fields' do 24 | it 'returns public JSON if you nil options' do 25 | example = JsonFoobar.create(foo: 'FOO', baz: 'BAZ', bar: 'BAR') 26 | expect(example.as_json(nil)).to eq(example.as_json(properties: :short)) 27 | end 28 | it 'allows subsets of fields to be returned by varying the properties definition' do 29 | example = JsonFoobar.create(foo: 'FOO', baz: 'BAZ', bar: 'BAR') 30 | # :short is a subset of the fields in :public and :public is a subset of the fields in :all 31 | expect(example.as_json(properties: :short)).to eq(:foo => 'FOO', 'Baz' => 'BAZ', :default_foo => 'DEFAULT_FOO') 32 | expect(example.as_json(properties: :public)).to eq(:foo => 'FOO', 'Baz' => 'BAZ', :bar => 'BAR', :default_foo => 'DEFAULT_FOO') 33 | expect(example.as_json(properties: :all)).to eq(:foo => 'FOO', :bar => 'BAR', 'Baz' => 'BAZ', :renamed_baz => 'BAZ', :default_foo => 'DEFAULT_FOO', :computed_field => 'FOOBAR') 34 | end 35 | it 'throws an error if you ask for an undefined property type' do 36 | expect { JsonFoobar.create.as_json(properties: :special) }.to raise_error(ArgumentError) 37 | end 38 | it "does not raise an error if you don't specify properties" do 39 | expect { JsonFoobar.create.as_json({}) }.to_not raise_error 40 | end 41 | it 'should hit the cache for subsequent as_json calls after the first' do 42 | foobar = JsonFoobar.create(foo: 'FOO', bar: 'BAR', baz: 'BAZ') 43 | all_result = foobar.as_json(properties: :all) 44 | public_result = foobar.as_json(properties: :public) 45 | short_result = foobar.as_json(properties: :short) 46 | expect(all_result).not_to eq(public_result) 47 | expect(all_result).not_to eq(short_result) 48 | expect(public_result).not_to eq(short_result) 49 | 3.times { expect(foobar.as_json(properties: :all)).to eq(all_result) } 50 | 3.times { expect(foobar.as_json(properties: :public)).to eq(public_result) } 51 | 3.times { expect(foobar.as_json(properties: :short)).to eq(short_result) } 52 | end 53 | it 'should remove values from the cache when a model is saved' do 54 | foobar = JsonFoobar.create(foo: 'FOO', bar: 'BAR', baz: 'BAZ') 55 | all_result = foobar.as_json(properties: :all) 56 | public_result = foobar.as_json(properties: :public) 57 | short_result = foobar.as_json(properties: :short) 58 | foobar.foo = 'updated' 59 | # Not saved yet, so we should still be hitting the cache 60 | 3.times { expect(foobar.as_json(properties: :all)).to eq(all_result) } 61 | 3.times { expect(foobar.as_json(properties: :public)).to eq(public_result) } 62 | 3.times { expect(foobar.as_json(properties: :short)).to eq(short_result) } 63 | foobar.save 64 | 3.times { expect(foobar.as_json(properties: :all)).to eq(all_result.merge(foo: 'updated', computed_field: 'updatedBAR')) } 65 | 3.times { expect(foobar.as_json(properties: :public)).to eq(public_result.merge(foo: 'updated')) } 66 | 3.times { expect(foobar.as_json(properties: :short)).to eq(short_result.merge(foo: 'updated')) } 67 | end 68 | end 69 | context 'invalidate callbacks' do 70 | before :each do 71 | @foobar = JsonFoobar.create!(foo: 'FOO') 72 | end 73 | it 'should invalidate cache when a model is saved' do 74 | expect do 75 | @foobar.update_attributes!(foo: 'BAR') 76 | end.to invalidate @foobar 77 | end 78 | it 'should also invalidate cache when a model is saved without changes' do 79 | expect do 80 | @foobar.save! 81 | end.to invalidate @foobar 82 | end 83 | it 'should invalidate cache when a model is destroyed' do 84 | expect do 85 | @foobar.destroy 86 | end.to invalidate @foobar 87 | end 88 | end 89 | context 'many-to-one relationships' do 90 | it 'uses the correct properties on the base object and passes :short or :all as appropriate' do 91 | manager = JsonManager.create!(name: 'Boss') 92 | peon = manager.json_employees.create!(name: 'Peon') 93 | manager.json_employees.create!(name: 'Indentured servant') 94 | manager.json_employees.create!(name: 'Serf', nickname: 'Vince') 95 | 3.times do 96 | 3.times do 97 | manager_short_json = manager.as_json(properties: :short) 98 | expect(manager_short_json.length).to eq(2) 99 | expect(manager_short_json[:name]).to eq('Boss') 100 | expect(manager_short_json[:employees].member?(name: 'Peon')).to be_truthy 101 | expect(manager_short_json[:employees].member?(name: 'Indentured servant')).to be_truthy 102 | expect(manager_short_json[:employees].member?(name: 'Serf')).to be_truthy 103 | expect(manager_short_json[:employees].member?(nickname: 'Serf')).to be_falsey 104 | end 105 | 3.times do 106 | manager_public_json = manager.as_json(properties: :public) 107 | expect(manager_public_json.length).to eq(2) 108 | expect(manager_public_json[:name]).to eq('Boss') 109 | expect(manager_public_json[:employees].member?(name: 'Peon')).to be_truthy 110 | expect(manager_public_json[:employees].member?(name: 'Indentured servant')).to be_truthy 111 | expect(manager_public_json[:employees].member?(name: 'Serf')).to be_truthy 112 | expect(manager_public_json[:employees].member?(nickname: 'Serf')).to be_falsey 113 | end 114 | 3.times do 115 | manager_all_json = manager.as_json(properties: :all) 116 | expect(manager_all_json.length).to eq(3) 117 | expect(manager_all_json[:name]).to eq('Boss') 118 | expect(manager_all_json[:ssn]).to eq('123-45-6789') 119 | expect(manager_all_json[:employees].member?(name: 'Peon', nickname: 'My Favorite')).to be_truthy 120 | expect(manager_all_json[:employees].member?(name: 'Indentured servant', nickname: 'My Favorite')).to be_truthy 121 | expect(manager_all_json[:employees].member?(name: 'Serf', nickname: 'Vince')).to be_truthy 122 | end 123 | 3.times do 124 | expect(peon.as_json(properties: :short)).to eq(name: 'Peon') 125 | end 126 | 3.times do 127 | expect(peon.as_json(properties: :all)).to eq(name: 'Peon', nickname: 'My Favorite') 128 | end 129 | end 130 | end 131 | it 'correctly updates fields when either the parent or child class changes' do 132 | manager = JsonManager.create!(name: 'JsonManager') 133 | employee = manager.json_employees.create!(name: 'JsonEmployee') 134 | 3.times do 135 | expect(manager.as_json(properties: :short)).to eq(name: 'JsonManager', employees: [{ name: 'JsonEmployee' }]) 136 | expect(employee.as_json(properties: :short)).to eq(name: 'JsonEmployee') 137 | end 138 | manager.name = 'New JsonManager' 139 | manager.save 140 | 3.times { expect(manager.as_json(properties: :short)).to eq(name: 'New JsonManager', employees: [{ name: 'JsonEmployee' }]) } 141 | employee.name = 'New JsonEmployee' 142 | employee.save 143 | 3.times { expect(manager.as_json(properties: :short)).to eq(name: 'New JsonManager', employees: [{ name: 'New JsonEmployee' }]) } 144 | end 145 | context 'reference_properties' do 146 | it 'limits the json fields of a child relationship' do 147 | supervisor = JsonSupervisor.create(name: 'JsonSupervisor') 148 | manager = JsonManager.create(name: 'JsonManager', supervisor: supervisor) 149 | json = supervisor.as_json(properties: :all) 150 | expect(json[:managers][0].key?(:ssn)).to be_falsey 151 | end 152 | end 153 | end 154 | context 'one-to-one relationships' do 155 | before(:each) do 156 | @artwork = AwesomeArtwork.create(name: 'Mona Lisa') 157 | end 158 | it 'uses the correct properties on the base object and passes :all to any sub-objects for :all properties' do 159 | 3.times do 160 | expect(@artwork.as_json(properties: :all)).to eq(name: 'Mona Lisa', image: nil) 161 | end 162 | end 163 | context 'with the relationship present' do 164 | before(:each) do 165 | @image = @artwork.create_awesome_image(name: 'Picture of Mona Lisa') 166 | end 167 | it 'uses the correct properties on the base object and passes :short to any sub-objects for :public and :short properties' do 168 | 3.times do 169 | expect(@artwork.as_json(properties: :short)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Mona' }) 170 | expect(@artwork.as_json(properties: :public)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Mona' }) 171 | expect(@artwork.as_json(properties: :all)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Mona', url: 'http://example.com/404.html' }) 172 | expect(@image.as_json(properties: :short)).to eq(name: 'Picture of Mona Lisa', nickname: 'Mona') 173 | expect(@image.as_json(properties: :public)).to eq(name: 'Picture of Mona Lisa', nickname: 'Mona', url: 'http://example.com/404.html') 174 | end 175 | end 176 | it 'uses the correct properties on the base object and passes :all to any sub-objects for :all properties' do 177 | 3.times do 178 | expect(@artwork.as_json(properties: :all)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Mona', url: 'http://example.com/404.html' }) 179 | end 180 | end 181 | it 'correctly updates fields when either the parent or child class changes' do 182 | # Call as_json for all properties so that the json will get cached 183 | [:short, :public, :all].each { |properties| @artwork.as_json(properties: properties) } 184 | @image.nickname = 'Worst Painting Ever' 185 | # Nothing has been saved yet, cached json for referenced document should reflect the truth in the database 186 | 3.times do 187 | expect(@artwork.as_json(properties: :short)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Mona' }) 188 | expect(@artwork.as_json(properties: :public)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Mona' }) 189 | expect(@artwork.as_json(properties: :all)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Mona', url: 'http://example.com/404.html' }) 190 | end 191 | @image.save 192 | 3.times do 193 | expect(@artwork.as_json(properties: :short)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Worst Painting Ever' }) 194 | expect(@artwork.as_json(properties: :public)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Worst Painting Ever' }) 195 | expect(@artwork.as_json(properties: :all)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Worst Painting Ever', url: 'http://example.com/404.html' }) 196 | end 197 | @image.name = 'Picture of Mona Lisa Watercolor' 198 | 3.times do 199 | expect(@artwork.as_json(properties: :short)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Worst Painting Ever' }) 200 | expect(@artwork.as_json(properties: :public)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Worst Painting Ever' }) 201 | expect(@artwork.as_json(properties: :all)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa', nickname: 'Worst Painting Ever', url: 'http://example.com/404.html' }) 202 | end 203 | @image.save 204 | 3.times do 205 | expect(@artwork.as_json(properties: :short)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa Watercolor', nickname: 'Worst Painting Ever' }) 206 | expect(@artwork.as_json(properties: :public)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa Watercolor', nickname: 'Worst Painting Ever' }) 207 | expect(@artwork.as_json(properties: :all)).to eq(name: 'Mona Lisa', image: { name: 'Picture of Mona Lisa Watercolor', nickname: 'Worst Painting Ever', url: 'http://example.com/404.html' }) 208 | end 209 | end 210 | end 211 | end 212 | context 'with a hide_as_child_json_when definition' do 213 | it 'should yield JSON when as_json is called directly and hide_as_child_json_when returns false on an instance' do 214 | c = SometimesSecret.create(should_tell_secret: true) 215 | expect(c.as_json(properties: :short)).to eq(secret: 'Afraid of the dark') 216 | end 217 | it 'should yield JSON when as_json is called directly and hide_as_child_json_when returns true on an instance' do 218 | c = SometimesSecret.create(should_tell_secret: false) 219 | expect(c.as_json(properties: :short)).to eq(secret: 'Afraid of the dark') 220 | end 221 | it 'should yield JSON without an instance of a child' do 222 | p = SecretParent.create(name: 'Parent') 223 | expect(p.as_json(properties: :all)[:child]).to be_nil 224 | end 225 | it 'should yield child JSON when as_json is called on the parent and hide_as_child_json_when returns false on an instance' do 226 | p = SecretParent.create(name: 'Parent') 227 | p.create_sometimes_secret(should_tell_secret: true) 228 | expect(p.as_json(properties: :short)[:child]).to eq(secret: 'Afraid of the dark') 229 | end 230 | it 'should not yield child JSON when as_json is called on the parent and hide_as_child_json_when returns true on an instance' do 231 | p = SecretParent.create(name: 'Parent') 232 | p.create_sometimes_secret(should_tell_secret: false) 233 | expect(p.as_json(properties: :short)).to eq(name: 'Parent', child: nil) 234 | expect(p.as_json(properties: :short)[:child]).to be_nil 235 | end 236 | end 237 | context 'relationships with a multi-level hierarchy' do 238 | before(:each) do 239 | @artwork = FastJsonArtwork.create 240 | @image = @artwork.create_fast_json_image 241 | @url1 = @image.fast_json_urls.create 242 | @url2 = @image.fast_json_urls.create 243 | @url3 = @image.fast_json_urls.create 244 | @common_url = @url1.url 245 | end 246 | it 'uses the correct properties on the base object and passes :short to any sub-objects for :short and :public' do 247 | 3.times do 248 | expect(@artwork.as_json(properties: :short)).to eq( 249 | name: 'Artwork', 250 | image: { name: 'Image', 251 | urls: [ 252 | { url: @common_url }, 253 | { url: @common_url }, 254 | { url: @common_url } 255 | ] 256 | } 257 | ) 258 | expect(@artwork.as_json(properties: :public)).to eq( 259 | name: 'Artwork', 260 | display_name: 'Awesome Artwork', 261 | image: { name: 'Image', 262 | urls: [ 263 | { url: @common_url }, 264 | { url: @common_url }, 265 | { url: @common_url } 266 | ] 267 | } 268 | ) 269 | end 270 | end 271 | it 'uses the correct properties on the base object and passes :all to any sub-objects for :all' do 272 | 3.times do 273 | expect(@artwork.as_json(properties: :all)).to eq( 274 | name: 'Artwork', 275 | display_name: 'Awesome Artwork', 276 | price: 1000, 277 | image: { name: 'Image', 278 | urls: [ 279 | { url: @common_url, is_public: false }, 280 | { url: @common_url, is_public: false }, 281 | { url: @common_url, is_public: false }] 282 | } 283 | ) 284 | end 285 | end 286 | it 'correctly updates json for all classes in the hierarchy when saves occur' do 287 | # Call as_json once to make sure the json is cached before we modify the referenced model locally 288 | @artwork.as_json(properties: :short) 289 | new_url = 'http://chee.sy/omg.jpg' 290 | @url1.url = new_url 291 | # No save has happened, so as_json shouldn't update yet 292 | 3.times do 293 | expect(@artwork.as_json(properties: :short)).to eq( 294 | name: 'Artwork', 295 | image: { name: 'Image', 296 | urls: [ 297 | { url: @common_url }, 298 | { url: @common_url }, 299 | { url: @common_url } 300 | ] 301 | } 302 | ) 303 | end 304 | @url1.save 305 | 3.times do 306 | json = @artwork.as_json 307 | expect(json[:name]).to eq('Artwork') 308 | expect(json[:image][:name]).to eq('Image') 309 | expect(json[:image][:urls].map { |u| u[:url] }.sort).to eq([@common_url, @common_url, new_url].sort) 310 | end 311 | end 312 | end 313 | context 'transform' do 314 | context 'upcase' do 315 | before :each do 316 | Mongoid::CachedJson.config.transform do |_field, _definition, value| 317 | value.upcase 318 | end 319 | end 320 | it 'transforms every value in returned JSON' do 321 | expect(JsonFoobar.new(foo: 'foo', bar: 'Bar', baz: 'BAZ').as_json).to eq('Baz' => 'BAZ', :default_foo => 'DEFAULT_FOO', :foo => 'FOO') 322 | end 323 | end 324 | context 'with options' do 325 | before :each do 326 | Mongoid::CachedJson.config.transform do |_field, definition, value| 327 | definition[:transform] ? value.send(definition[:transform].to_sym) : value 328 | end 329 | end 330 | it 'transforms every value in returned JSON using the :transform attribute' do 331 | expect(JsonTransform.new(upcase: 'upcase', downcase: 'DOWNCASE', nochange: 'eLiTe').as_json).to eq(upcase: 'UPCASE', downcase: 'downcase', nochange: 'eLiTe') 332 | end 333 | end 334 | context 'with multiple transformations' do 335 | before :each do 336 | Mongoid::CachedJson.config.transform do |_field, _definition, value| 337 | value.to_i + 1 338 | end 339 | Mongoid::CachedJson.config.transform do |_field, _definition, value| 340 | value.to_i / 2 341 | end 342 | end 343 | it 'transforms every value in returned JSON using the :transform attribute' do 344 | expect(JsonMath.new(number: 9).as_json).to eq(number: 5) 345 | end 346 | end 347 | end 348 | context 'with cache disabled' do 349 | before :each do 350 | allow(Mongoid::CachedJson.config).to receive(:disable_caching).and_return(true) 351 | end 352 | it 'forces a cache miss' do 353 | example = JsonFoobar.create(foo: 'FOO', baz: 'BAZ', bar: 'BAR') 354 | key = "as_json/unspecified/JsonFoobar/#{example.id}/short/true" 355 | case cache_store 356 | when :memory_store then 357 | expect(Mongoid::CachedJson.config.cache).to receive(:fetch).with(key, force: true).twice 358 | when :dalli_store then 359 | expect(Mongoid::CachedJson.config.cache).not_to receive(:write) 360 | else 361 | fail ArgumentError, "invalid cache store: #{cache_store}" 362 | end 363 | 2.times { example.as_json } 364 | end 365 | end 366 | context 'versioning' do 367 | it 'returns JSON for version 2' do 368 | example = JsonFoobar.create(foo: 'FOO', baz: 'BAZ', bar: 'BAR') 369 | expect(example.as_json(properties: :short, version: :v2)).to eq(:foo => 'FOO', 'Taz' => 'BAZ', 'Naz' => 'BAZ', :default_foo => 'DEFAULT_FOO') 370 | end 371 | it 'returns JSON for version 3' do 372 | example = JsonFoobar.create(foo: 'FOO', baz: 'BAZ', bar: 'BAR') 373 | expect(example.as_json(properties: :short, version: :v3)).to eq(:foo => 'FOO', 'Naz' => 'BAZ', :default_foo => 'DEFAULT_FOO') 374 | end 375 | it "returns default JSON for version 4 that hasn't been declared" do 376 | example = JsonFoobar.create(foo: 'FOO', baz: 'BAZ', bar: 'BAR') 377 | expect(example.as_json(properties: :short, version: :v4)).to eq(foo: 'FOO', default_foo: 'DEFAULT_FOO') 378 | end 379 | it 'returns JSON for the default version' do 380 | Mongoid::CachedJson.config.default_version = :v2 381 | example = JsonFoobar.create(foo: 'FOO', baz: 'BAZ', bar: 'BAR') 382 | expect(example.as_json(properties: :short)).to eq(:foo => 'FOO', 'Taz' => 'BAZ', 'Naz' => 'BAZ', :default_foo => 'DEFAULT_FOO') 383 | end 384 | it 'returns correct JSON for Person used in README' do 385 | person = Person.create(first: 'John', middle: 'F.', last: 'Kennedy', born: 'May 29, 1917') 386 | expect(person.as_json).to eq(name: 'John F. Kennedy') 387 | expect(person.as_json(version: :v2)).to eq(first: 'John', middle: 'F.', last: 'Kennedy', name: 'John F. Kennedy') 388 | expect(person.as_json(version: :v3)).to eq(first: 'John', middle: 'F.', last: 'Kennedy', name: 'John F. Kennedy', born: 'May 29, 1917') 389 | end 390 | end 391 | context 'polymorphic objects' do 392 | before(:each) do 393 | @json_embedded_foobar = JsonEmbeddedFoobar.new(foo: 'embedded') 394 | @json_referenced_foobar = JsonReferencedFoobar.new(foo: 'referenced') 395 | @json_parent_foobar = JsonParentFoobar.create( 396 | json_polymorphic_embedded_foobar: @json_embedded_foobar, 397 | json_polymorphic_referenced_foobar: @json_referenced_foobar 398 | ) 399 | @json_referenced_foobar.save! 400 | 401 | # Cache... 402 | [:all, :short, :public].each do |prop| 403 | @json_parent_foobar.as_json(properties: prop) 404 | end 405 | end 406 | it 'returns correct JSON when a child (embedded) polymorphic document is changed' do 407 | expect(@json_parent_foobar.as_json(properties: :all)[:json_polymorphic_embedded_foobar][:foo]).to eq('embedded') 408 | expect(@json_embedded_foobar.as_json(properties: :all)[:foo]).to eq('embedded') 409 | @json_embedded_foobar.update_attributes!(foo: 'EMBEDDED') 410 | expect(@json_embedded_foobar.as_json(properties: :all)[:foo]).to eq('EMBEDDED') 411 | expect(@json_parent_foobar.as_json(properties: :all)[:json_polymorphic_embedded_foobar][:foo]).to eq('EMBEDDED') 412 | end 413 | it 'returns correct JSON when a child (referenced) polymorphic document is changed' do 414 | expect(@json_parent_foobar.as_json(properties: :all)[:json_polymorphic_referenced_foobar][:foo]).to eq('referenced') 415 | expect(@json_referenced_foobar.as_json(properties: :all)[:foo]).to eq('referenced') 416 | @json_referenced_foobar.update_attributes!(foo: 'REFERENCED') 417 | expect(@json_referenced_foobar.as_json(properties: :all)[:foo]).to eq('REFERENCED') 418 | expect(@json_parent_foobar.as_json(properties: :all)[:json_polymorphic_referenced_foobar][:foo]).to eq('REFERENCED') 419 | end 420 | end 421 | context 'polymorphic relationships' do 422 | before :each do 423 | @company = PolyCompany.create! 424 | @company_post = PolyPost.create!(postable: @company) 425 | @person = PolyPerson.create! 426 | @person_post = PolyPost.create!(postable: @person) 427 | end 428 | it 'returns the correct JSON' do 429 | expect(@company_post.as_json).to eq(parent: { id: @company.id, type: 'PolyCompany' }) 430 | expect(@person_post.as_json).to eq(parent: { id: @person.id, type: 'PolyPerson' }) 431 | end 432 | end 433 | context 'cache key' do 434 | it 'correctly generates a cached json key' do 435 | example = JsonFoobar.create(foo: 'FOO', baz: 'BAZ', bar: 'BAR') 436 | expect(JsonFoobar.cached_json_key({ properties: :short, is_top_level_json: true, version: :v1 }, example.class, example.id)).to eq("as_json/v1/JsonFoobar/#{example.id}/short/true") 437 | end 438 | end 439 | context 'embeds_many relationships' do 440 | before :each do 441 | @cell = PrisonCell.create!(number: 42) 442 | @cell.inmates.create!(nickname: 'Joe', person: Person.create!(first: 'Joe')) 443 | @cell.inmates.create!(nickname: 'Bob', person: Person.create!(first: 'Bob')) 444 | end 445 | it 'returns the correct JSON' do 446 | expect(@cell.as_json(properties: :all)).to eq(number: 42, 447 | inmates: [ 448 | { nickname: 'Joe', person: { name: 'Joe' } }, 449 | { nickname: 'Bob', person: { name: 'Bob' } } 450 | ] 451 | ) 452 | end 453 | end 454 | context 'with repeated objects in the JSON' do 455 | before :each do 456 | @cell = PrisonCell.create!(number: 42) 457 | @person = Person.create!(first: 'Evil') 458 | @cell.inmates.create!(nickname: 'Joe', person: @person) 459 | @cell.inmates.create!(nickname: 'Bob', person: @person) 460 | end 461 | it 'returns the correct JSON' do 462 | expect(@cell.as_json(properties: :all)).to eq(number: 42, 463 | inmates: [ 464 | { nickname: 'Joe', person: { name: 'Evil' } }, 465 | { nickname: 'Bob', person: { name: 'Evil' } } 466 | ] 467 | ) 468 | end 469 | end 470 | context 'belongs_to relationship' do 471 | before :each do 472 | @tool = Tool.create!(name: 'hammer') 473 | end 474 | it 'returns a nil reference' do 475 | expect(@tool.as_json(properties: :all)).to eq(tool_box: nil, name: 'hammer') 476 | end 477 | context 'persisted' do 478 | before :each do 479 | @tool_box = ToolBox.create!(color: 'red') 480 | @tool.update_attributes!(tool_box: @tool_box) 481 | end 482 | it 'returns a reference' do 483 | expect(@tool.as_json(properties: :all)).to eq(tool_box: { color: 'red' }, name: 'hammer') 484 | end 485 | end 486 | end 487 | context 'many-to-many relationships' do 488 | before :each do 489 | @image = FastJsonImage.create! 490 | end 491 | it 'resolves a default empty relationship' do 492 | expect(@image.as_json(properties: :all)).to eq(name: 'Image', urls: []) 493 | end 494 | it 'resolves a nil relationship on destroy' do 495 | @image.destroy 496 | expect(@image.as_json(properties: :all)).to eq(name: 'Image', urls: []) 497 | end 498 | end 499 | end 500 | end 501 | end 502 | -------------------------------------------------------------------------------- /spec/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::CachedJson::Config do 4 | before :each do 5 | @cache = Mongoid::CachedJson::Config.cache 6 | end 7 | after :each do 8 | Mongoid::CachedJson::Config.cache = @cache 9 | end 10 | it 'configures a cache store' do 11 | cache = Class.new 12 | Mongoid::CachedJson.configure do |config| 13 | config.cache = cache 14 | end 15 | expect(cache).to receive(:fetch).once 16 | JsonFoobar.new.as_json 17 | end 18 | it 'sets disable_caching to false' do 19 | expect(Mongoid::CachedJson.config.disable_caching).to be_falsey 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/dalli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'active_support/cache/dalli_store' 3 | 4 | describe ActiveSupport::Cache::DalliStore do 5 | before :each do 6 | @cache = Mongoid::CachedJson::Config.cache 7 | Mongoid::CachedJson.configure do |config| 8 | config.cache = ActiveSupport::Cache.lookup_store(:dalli_store) 9 | end 10 | end 11 | after :each do 12 | Mongoid::CachedJson::Config.cache = @cache 13 | end 14 | it 'uses dalli_store' do 15 | expect(Mongoid::CachedJson.config.cache).to be_a ActiveSupport::Cache::DalliStore 16 | end 17 | context 'read_multi' do 18 | context 'array' do 19 | it 'uses a local cache to fetch repeated objects' do 20 | options = { properties: :all, is_top_level_json: true, version: :unspecified } 21 | tool1 = Tool.create!(name: 'hammer') 22 | tool1_key = Tool.cached_json_key(options, Tool, tool1.id) 23 | tool2 = Tool.create!(name: 'screwdriver') 24 | tool2_key = Tool.cached_json_key(options, Tool, tool2.id) 25 | expect(Mongoid::CachedJson.config.cache).not_to receive(:fetch) 26 | expect(Mongoid::CachedJson.config.cache).to receive(:read_multi).with(tool1_key, tool2_key).once.and_return( 27 | tool1_key => { _id: tool1.id.to_s }, 28 | tool2_key => { _id: tool2.id.to_s } 29 | ) 30 | expect([tool1, tool2].as_json(properties: :all)).to eq([ 31 | { tool_box: nil, _id: tool1.id.to_s }, 32 | { tool_box: nil, _id: tool2.id.to_s } 33 | ]) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/hash_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hash do 4 | it 'hash' do 5 | expect({ x: 'x', y: 'y' }.as_json).to eq(x: 'x', y: 'y') 6 | end 7 | it 'materializes multiple objects that may or may not respond to as_json_partial' do 8 | foobar1 = JsonFoobar.create(foo: 'FOO1', baz: 'BAZ', bar: 'BAR') 9 | foobar2 = JsonFoobar.create(foo: 'FOO2', baz: 'BAZ', bar: 'BAR') 10 | expect({ 11 | :x => :y, 12 | :foobar1 => foobar1, 13 | :foobar2 => foobar2, 14 | :z => { 15 | foobar1: foobar1 16 | }, 17 | :t => [foobar1, :y], 18 | 'empty' => [] 19 | }.as_json).to eq( 20 | :x => 'y', 21 | :foobar1 => { :foo => 'FOO1', 'Baz' => 'BAZ', :default_foo => 'DEFAULT_FOO' }, 22 | :foobar2 => { :foo => 'FOO2', 'Baz' => 'BAZ', :default_foo => 'DEFAULT_FOO' }, 23 | :z => { foobar1: { :foo => 'FOO1', 'Baz' => 'BAZ', :default_foo => 'DEFAULT_FOO' } }, 24 | :t => [{ :foo => 'FOO1', 'Baz' => 'BAZ', :default_foo => 'DEFAULT_FOO' }, 'y'], 25 | 'empty' => [] 26 | ) 27 | end 28 | context 'without read_multi' do 29 | before :each do 30 | Mongoid::CachedJson.config.cache.instance_eval { undef :read_multi } 31 | end 32 | it 'uses a local cache to fetch repeated objects' do 33 | tool = Tool.create!(name: 'hammer') 34 | expect(Mongoid::CachedJson.config.cache).to receive(:fetch).once.and_return( 35 | x: :y 36 | ) 37 | expect({ 38 | t1: tool, 39 | t2: tool, 40 | t3: tool 41 | }.as_json(properties: :all)).to eq( 42 | t1: { tool_box: nil, x: :y }, 43 | t2: { tool_box: nil, x: :y }, 44 | t3: { tool_box: nil, x: :y } 45 | ) 46 | end 47 | end 48 | context 'with read_multi' do 49 | it 'uses a local cache to fetch repeated objects' do 50 | tool = Tool.create!(name: 'hammer') 51 | tool_key = "as_json/unspecified/Tool/#{tool.id}/all/true" 52 | expect(Mongoid::CachedJson.config.cache).to receive(:read_multi).once.with(tool_key).and_return( 53 | tool_key => { x: :y } 54 | ) 55 | expect({ 56 | t1: tool, 57 | t2: tool, 58 | t3: tool 59 | }.as_json(properties: :all)).to eq( 60 | t1: { tool_box: nil, x: :y }, 61 | t2: { tool_box: nil, x: :y }, 62 | t3: { tool_box: nil, x: :y } 63 | ) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/mongoid_criteria_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Mongoid::Criteria do 4 | it 'mongoid_criteria' do 5 | tool_box = ToolBox.create!(color: 'red') 6 | Tool.create!(name: 'hammer', tool_box: tool_box) 7 | Tool.create!(name: 'screwdriver', tool_box: tool_box) 8 | expect(Tool.where(tool_box_id: tool_box.id).as_json(properties: :all)).to eq([ 9 | { tool_box: { color: 'red' }, name: 'hammer' }, 10 | { tool_box: { color: 'red' }, name: 'screwdriver' } 11 | ]) 12 | end 13 | context 'without read_multi' do 14 | before :each do 15 | Mongoid::CachedJson.config.cache.instance_eval { undef :read_multi } 16 | end 17 | it 'materializes multiple objects using a single partial' do 18 | tool_box = ToolBox.create!(color: 'red') 19 | Tool.create!(name: 'hammer', tool_box: tool_box) 20 | Tool.create!(name: 'screwdriver', tool_box: tool_box) 21 | # once per tool and once for the tool box 22 | expect(Mongoid::CachedJson.config.cache).to receive(:fetch).exactly(3).times.and_return( 23 | x: :y 24 | ) 25 | expect(tool_box.tools.as_json(properties: :all)).to eq([ 26 | { tool_box: { x: :y }, x: :y }, 27 | { tool_box: { x: :y }, x: :y } 28 | ]) 29 | end 30 | end 31 | context 'with read_multi' do 32 | it 'responds to read_multi' do 33 | expect(Mongoid::CachedJson.config.cache).to respond_to :read_multi 34 | end 35 | it 'materializes multiple objects using a single partial' do 36 | tool_box = ToolBox.create!(color: 'red') 37 | tool1 = Tool.create!(name: 'hammer', tool_box: tool_box) 38 | tool2 = Tool.create!(name: 'screwdriver', tool_box: tool_box) 39 | # once per tool and once for the tool box 40 | keys = [ 41 | "as_json/unspecified/Tool/#{tool1.id}/all/true", 42 | "as_json/unspecified/ToolBox/#{tool_box.id}/all/false", 43 | "as_json/unspecified/Tool/#{tool2.id}/all/true" 44 | ] 45 | expect(Mongoid::CachedJson.config.cache).to receive(:read_multi).once.with(*keys).and_return( 46 | keys[0] => { x: :y }, 47 | keys[1] => { x: :y }, 48 | keys[2] => { x: :y } 49 | ) 50 | expect(tool_box.tools.as_json(properties: :all)).to eq([ 51 | { tool_box: { x: :y }, x: :y }, 52 | { tool_box: { x: :y }, x: :y } 53 | ]) 54 | end 55 | it 'does not call fetch for missing objects, only write' do 56 | tool_box = ToolBox.create!(color: 'red') 57 | tool1 = Tool.create!(name: 'hammer', tool_box: tool_box) 58 | tool2 = Tool.create!(name: 'screwdriver', tool_box: tool_box) 59 | # once per tool and once for the tool box 60 | keys = [ 61 | "as_json/unspecified/Tool/#{tool1.id}/all/true", 62 | "as_json/unspecified/ToolBox/#{tool_box.id}/all/false", 63 | "as_json/unspecified/Tool/#{tool2.id}/all/true" 64 | ] 65 | expect(Mongoid::CachedJson.config.cache).to receive(:read_multi).once.with(*keys).and_return( 66 | keys[0] => { x: :y }, 67 | keys[1] => { x: :y } 68 | ) 69 | # read_multi returned only 2 of 3 things, don't call fetch, just store the third value 70 | expect(Mongoid::CachedJson.config.cache).not_to receive(:fetch) 71 | expect(Mongoid::CachedJson.config.cache).to receive(:write).with("as_json/unspecified/Tool/#{tool2.id}/all/true", name: 'screwdriver') 72 | expect(tool_box.tools.as_json(properties: :all)).to eq([ 73 | { tool_box: { x: :y }, x: :y }, 74 | { tool_box: { x: :y }, name: 'screwdriver' } 75 | ]) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | 4 | require 'rspec' 5 | require 'mongoid-cached-json' 6 | require 'mongoid-compatibility' 7 | 8 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each do |f| 9 | require f 10 | end 11 | 12 | Mongoid.configure do |config| 13 | config.connect_to('cached_json_test') 14 | end 15 | 16 | RSpec.configure do |config| 17 | config.raise_errors_for_deprecations! 18 | config.after :each do 19 | Mongoid::CachedJson.config.reset! 20 | end 21 | config.before :all do 22 | Mongoid.logger.level = Logger::INFO 23 | Mongo::Logger.logger.level = Logger::INFO if Mongoid::Compatibility::Version.mongoid5? 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/support/awesome_artwork.rb: -------------------------------------------------------------------------------- 1 | class AwesomeArtwork 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :name 6 | has_one :awesome_image 7 | 8 | json_fields \ 9 | name: {}, 10 | image: { type: :reference, definition: :awesome_image } 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/awesome_image.rb: -------------------------------------------------------------------------------- 1 | class AwesomeImage 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :name 6 | field :nickname, default: 'Mona' 7 | field :url, default: 'http://example.com/404.html' 8 | belongs_to :awesome_artwork 9 | 10 | json_fields \ 11 | name: {}, 12 | nickname: {}, 13 | url: { properties: :public } 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/fast_json_artwork.rb: -------------------------------------------------------------------------------- 1 | class FastJsonArtwork 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :name, default: 'Artwork' 6 | field :display_name, default: 'Awesome Artwork' 7 | field :price, default: 1000 8 | has_one :fast_json_image 9 | 10 | json_fields \ 11 | name: {}, 12 | display_name: { properties: :public }, 13 | price: { properties: :all }, 14 | image: { type: :reference, definition: :fast_json_image } 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/fast_json_image.rb: -------------------------------------------------------------------------------- 1 | class FastJsonImage 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :name, default: 'Image' 6 | if Mongoid::Compatibility::Version.mongoid5_or_older? 7 | belongs_to :fast_json_artwork 8 | else 9 | belongs_to :fast_json_artwork, required: false 10 | end 11 | has_and_belongs_to_many :fast_json_urls 12 | 13 | json_fields \ 14 | name: {}, 15 | urls: { type: :reference, definition: :fast_json_urls } 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/fast_json_url.rb: -------------------------------------------------------------------------------- 1 | class FastJsonUrl 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :url, default: 'http://art.sy/omg.jpeg' 6 | field :is_public, default: false 7 | has_and_belongs_to_many :fast_json_image 8 | 9 | json_fields \ 10 | url: {}, 11 | is_public: { properties: :all } 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/json_embedded_foobar.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'json_polymorphic_embedded_foobar') 2 | 3 | class JsonEmbeddedFoobar < JsonPolymorphicEmbeddedFoobar 4 | field :foo, type: String 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/json_employee.rb: -------------------------------------------------------------------------------- 1 | class JsonEmployee 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :name 6 | field :nickname, default: 'My Favorite' 7 | belongs_to :json_manager 8 | 9 | json_fields \ 10 | name: {}, 11 | nickname: { properties: :all } 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/json_foobar.rb: -------------------------------------------------------------------------------- 1 | class JsonFoobar 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :foo 6 | field :bar 7 | field :baz 8 | field :default_foo, default: 'DEFAULT_FOO' 9 | 10 | json_fields \ 11 | :foo => { properties: :short }, 12 | :bar => { properties: :public }, 13 | 'Baz' => { definition: :baz, version: :unspecified }, 14 | 'Taz' => { definition: :baz, version: :v2 }, 15 | 'Naz' => { definition: :baz, versions: [:v2, :v3] }, 16 | :renamed_baz => { properties: :all, definition: :baz }, 17 | :default_foo => {}, # default value for properties is :short 18 | :computed_field => { properties: :all, definition: lambda { |x| "#{x.foo}#{x.bar}" } } 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/json_manager.rb: -------------------------------------------------------------------------------- 1 | class JsonManager 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :name 6 | field :ssn, default: '123-45-6789' 7 | has_many :json_employees 8 | 9 | if Mongoid::Compatibility::Version.mongoid5_or_older? 10 | belongs_to :supervisor, class_name: 'JsonSupervisor' 11 | else 12 | belongs_to :supervisor, class_name: 'JsonSupervisor', required: false 13 | end 14 | 15 | json_fields \ 16 | name: {}, 17 | ssn: { properties: :all }, 18 | employees: { type: :reference, definition: :json_employees } 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/json_math.rb: -------------------------------------------------------------------------------- 1 | class JsonMath 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :number 6 | 7 | json_fields \ 8 | number: {} 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/json_parent_foobar.rb: -------------------------------------------------------------------------------- 1 | class JsonParentFoobar 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | belongs_to :json_polymorphic_referenced_foobar 6 | embeds_one :json_polymorphic_embedded_foobar 7 | 8 | json_fields \ 9 | json_polymorphic_referenced_foobar: { type: :reference }, 10 | json_polymorphic_embedded_foobar: { type: :reference } 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/json_polymorphic_embedded_foobar.rb: -------------------------------------------------------------------------------- 1 | class JsonPolymorphicEmbeddedFoobar 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | embedded_in :json_parent_foobar 6 | 7 | json_fields \ 8 | foo: { properties: :short } 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/json_polymorphic_referenced_foobar.rb: -------------------------------------------------------------------------------- 1 | class JsonPolymorphicReferencedFoobar 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | has_one :json_parent_foobar 6 | 7 | json_fields \ 8 | foo: { properties: :short } 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/json_referenced_foobar.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'json_polymorphic_referenced_foobar') 2 | 3 | class JsonReferencedFoobar < JsonPolymorphicReferencedFoobar 4 | field :foo, type: String 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/json_supervisor.rb: -------------------------------------------------------------------------------- 1 | class JsonSupervisor 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :name 6 | field :ssn, default: '123-45-6789' 7 | has_many :json_managers 8 | 9 | json_fields \ 10 | name: {}, 11 | ssn: { properties: :all }, 12 | managers: { type: :reference, definition: :json_managers, properties: :all, reference_properties: :short } 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/json_transform.rb: -------------------------------------------------------------------------------- 1 | class JsonTransform 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :upcase 6 | field :downcase 7 | field :nochange 8 | 9 | json_fields \ 10 | upcase: { transform: :upcase }, 11 | downcase: { transform: :downcase }, 12 | nochange: {} 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/matchers/invalidate.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :invalidate do |target| 2 | supports_block_expectations 3 | 4 | match do |block| 5 | call_counts = 0 6 | Array(target).each do |cached_model| 7 | allow(cached_model).to receive(:expire_cached_json) do 8 | call_counts += 1 9 | end 10 | end 11 | block.call if block.is_a?(Proc) 12 | call_counts == Array(target).count 13 | end 14 | 15 | failure_message do 16 | 'target cache to be invalidated, but it was not' 17 | end 18 | 19 | failure_message_when_negated do 20 | 'target cache to be invalidated, but it was not' 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/support/person.rb: -------------------------------------------------------------------------------- 1 | class Person 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :first, type: String 6 | field :last, type: String 7 | field :middle, type: String 8 | field :born, type: String 9 | 10 | def name 11 | [first, middle, last].compact.join(' ') 12 | end 13 | 14 | json_fields \ 15 | first: { versions: [:v2, :v3] }, 16 | last: { versions: [:v2, :v3] }, 17 | middle: { versions: [:v2, :v3] }, 18 | born: { versions: :v3 }, 19 | name: { definition: :name } 20 | end 21 | -------------------------------------------------------------------------------- /spec/support/poly_company.rb: -------------------------------------------------------------------------------- 1 | class PolyCompany 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | has_many :poly_posts, as: :postable 6 | 7 | json_fields \ 8 | id: {}, 9 | type: { definition: lambda { |x| x.class.name } } 10 | end 11 | -------------------------------------------------------------------------------- /spec/support/poly_person.rb: -------------------------------------------------------------------------------- 1 | class PolyPerson 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | has_many :poly_posts, as: :postable 6 | 7 | json_fields \ 8 | id: {}, 9 | type: { definition: lambda { |x| x.class.name } } 10 | end 11 | -------------------------------------------------------------------------------- /spec/support/poly_post.rb: -------------------------------------------------------------------------------- 1 | class PolyPost 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | belongs_to :postable, polymorphic: true 6 | 7 | json_fields \ 8 | parent: { type: :reference, definition: :postable } 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/prison_cell.rb: -------------------------------------------------------------------------------- 1 | class PrisonCell 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :number 6 | embeds_many :inmates, class_name: 'PrisonInmate' 7 | 8 | json_fields \ 9 | number: {}, 10 | inmates: { type: :reference } 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/prison_inmate.rb: -------------------------------------------------------------------------------- 1 | class PrisonInmate 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :nickname 6 | embedded_in :prison_cell, inverse_of: :inmates 7 | belongs_to :person 8 | 9 | json_fields \ 10 | nickname: {}, 11 | person: { type: :reference } 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/secret_parent.rb: -------------------------------------------------------------------------------- 1 | class SecretParent 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :name 6 | has_one :sometimes_secret 7 | 8 | json_fields \ 9 | name: {}, 10 | child: { definition: :sometimes_secret, type: :reference } 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/sometimes_secret.rb: -------------------------------------------------------------------------------- 1 | class SometimesSecret 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :secret, default: 'Afraid of the dark' 6 | field :should_tell_secret, type: Boolean 7 | belongs_to :secret_parent 8 | 9 | json_fields hide_as_child_json_when: lambda { |x| !x.should_tell_secret }, 10 | secret: {} 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/tool.rb: -------------------------------------------------------------------------------- 1 | class Tool 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :name 6 | 7 | if Mongoid::Compatibility::Version.mongoid5_or_older? 8 | belongs_to :tool_box 9 | else 10 | belongs_to :tool_box, required: false 11 | end 12 | 13 | json_fields \ 14 | name: {}, 15 | tool_box: { type: :reference } 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/tool_box.rb: -------------------------------------------------------------------------------- 1 | class ToolBox 2 | include Mongoid::Document 3 | include Mongoid::CachedJson 4 | 5 | field :color 6 | has_many :tools 7 | 8 | json_fields \ 9 | color: {} 10 | end 11 | --------------------------------------------------------------------------------