mongoid-cached-json 1.5.1 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +6 -0
  5. data/.rubocop_todo.yml +80 -0
  6. data/.travis.yml +16 -0
  7. data/CHANGELOG.md +85 -0
  8. data/CONTRIBUTING.md +118 -0
  9. data/Gemfile +21 -0
  10. data/README.md +49 -52
  11. data/Rakefile +34 -0
  12. data/lib/mongoid-cached-json.rb +0 -1
  13. data/lib/mongoid-cached-json/array.rb +1 -3
  14. data/lib/mongoid-cached-json/cached_json.rb +52 -50
  15. data/lib/mongoid-cached-json/config.rb +20 -21
  16. data/lib/mongoid-cached-json/hash.rb +1 -3
  17. data/lib/mongoid-cached-json/key_references.rb +0 -3
  18. data/lib/mongoid-cached-json/mongoid_criteria.rb +1 -3
  19. data/lib/mongoid-cached-json/version.rb +1 -2
  20. data/mongoid-cached-json.gemspec +17 -0
  21. data/spec/array_spec.rb +48 -0
  22. data/spec/benchmark_spec.rb +115 -0
  23. data/spec/cached_json_spec.rb +501 -0
  24. data/spec/config_spec.rb +21 -0
  25. data/spec/dalli_spec.rb +37 -0
  26. data/spec/hash_spec.rb +66 -0
  27. data/spec/mongoid_criteria_spec.rb +78 -0
  28. data/spec/spec_helper.rb +20 -0
  29. data/spec/support/awesome_artwork.rb +11 -0
  30. data/spec/support/awesome_image.rb +14 -0
  31. data/spec/support/fast_json_artwork.rb +15 -0
  32. data/spec/support/fast_json_image.rb +12 -0
  33. data/spec/support/fast_json_url.rb +12 -0
  34. data/spec/support/json_embedded_foobar.rb +5 -0
  35. data/spec/support/json_employee.rb +12 -0
  36. data/spec/support/json_foobar.rb +19 -0
  37. data/spec/support/json_manager.rb +14 -0
  38. data/spec/support/json_math.rb +9 -0
  39. data/spec/support/json_parent_foobar.rb +11 -0
  40. data/spec/support/json_polymorphic_embedded_foobar.rb +9 -0
  41. data/spec/support/json_polymorphic_referenced_foobar.rb +9 -0
  42. data/spec/support/json_referenced_foobar.rb +5 -0
  43. data/spec/support/json_supervisor.rb +13 -0
  44. data/spec/support/json_transform.rb +13 -0
  45. data/spec/support/matchers/invalidate.rb +22 -0
  46. data/spec/support/person.rb +20 -0
  47. data/spec/support/poly_company.rb +10 -0
  48. data/spec/support/poly_person.rb +10 -0
  49. data/spec/support/poly_post.rb +9 -0
  50. data/spec/support/prison_cell.rb +11 -0
  51. data/spec/support/prison_inmate.rb +12 -0
  52. data/spec/support/secret_parent.rb +11 -0
  53. data/spec/support/sometimes_secret.rb +11 -0
  54. data/spec/support/tool.rb +11 -0
  55. data/spec/support/tool_box.rb +10 -0
  56. metadata +62 -137
@@ -1,6 +1,5 @@
1
1
  module Mongoid
2
2
  class Criteria
3
-
4
3
  def as_json_partial(options = {})
5
4
  json_keys = nil
6
5
  json = map do |i|
@@ -12,13 +11,12 @@ module Mongoid
12
11
  i.as_json(options)
13
12
  end
14
13
  end
15
- [ json_keys, json ]
14
+ [json_keys, json]
16
15
  end
17
16
 
18
17
  def as_json(options = {})
19
18
  json_keys, json = as_json_partial(options)
20
19
  Mongoid::CachedJson.materialize_json_references_with_read_multi(json_keys, json)
21
20
  end
22
-
23
21
  end
24
22
  end
@@ -1,6 +1,5 @@
1
1
  module Mongoid
2
2
  module CachedJson
3
- VERSION = '1.5.1'
3
+ VERSION = '1.5.2'
4
4
  end
5
5
  end
6
-
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
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/dblock/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
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,115 @@
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
@@ -0,0 +1,501 @@
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 mutliple 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 'polymorhphic 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