mongoid-cached-json 1.5.1 → 1.5.2

Sign up to get free protection for your applications and to get access to all the features.
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