Skip to content

Commit

Permalink
Configuration for ETags and custom cache store
Browse files Browse the repository at this point in the history
  • Loading branch information
adamcrown committed Oct 9, 2019
1 parent 6b3c73f commit 7d2a3ba
Show file tree
Hide file tree
Showing 17 changed files with 243 additions and 92 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ Features
- **ETags** for easy HTTP caching
- **Simlpe, Readable DSL**

Configuration
-------------
### ETags
```ruby
CacheCrispies.configure do |conf|
conf.etags = true
end
```
_`etags` is set to `false` by default._

### Custom Cache Store
```ruby
CacheCrispies.configure do |conf|
conf.cache_store = ActiveSupport::Cache::DalliStore.new('localhost')
end
```
`cache_store` must be set to something that quacks like a [ActiveSupport::Cache::Store](https://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html).

_`cache_store` is set to `Rails.cache` by default, or `ActiveSupport::Cache::NullStore.new` if `Rails.cache` is `nil`._

Usage
-----
### A simple serializer
Expand Down
29 changes: 21 additions & 8 deletions lib/cache_crispies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,25 @@ module CacheCrispies
require 'cache_crispies/version'

# Use autoload for better Rails development
autoload :Attribute, 'cache_crispies/attribute'
autoload :Base, 'cache_crispies/base'
autoload :Collection, 'cache_crispies/collection'
autoload :Condition, 'cache_crispies/condition'
autoload :HashBuilder, 'cache_crispies/hash_builder'
autoload :Memoizer, 'cache_crispies/memoizer'
autoload :Controller, 'cache_crispies/controller'
autoload :Plan, 'cache_crispies/plan'
autoload :Attribute, 'cache_crispies/attribute'
autoload :Base, 'cache_crispies/base'
autoload :Collection, 'cache_crispies/collection'
autoload :Condition, 'cache_crispies/condition'
autoload :Configuration, 'cache_crispies/configuration'
autoload :HashBuilder, 'cache_crispies/hash_builder'
autoload :Memoizer, 'cache_crispies/memoizer'
autoload :Controller, 'cache_crispies/controller'
autoload :Plan, 'cache_crispies/plan'

def self.configure
yield config
end

def self.config
@config ||= Configuration.new
end

def self.cache
config.cache_store
end
end
2 changes: 1 addition & 1 deletion lib/cache_crispies/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def cached_json
hash[plan.cache_key] = model
end

Rails.cache.fetch_multi(models_by_cache_key.keys) do |cache_key|
CacheCrispies.cache.fetch_multi(models_by_cache_key.keys) do |cache_key|
model = models_by_cache_key[cache_key]

serializer.new(model, options).as_json
Expand Down
26 changes: 26 additions & 0 deletions lib/cache_crispies/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require 'rails'

module CacheCrispies
class Configuration
SETTINGS = [
:cache_store,
:etags
].freeze

SETTINGS.each do |setting|
attr_accessor setting
end

def initialize
reset!
end

alias etags? etags

# Resets all values to their defaults. Useful for testing.
def reset!
@cache_store = Rails.cache || ActiveSupport::Cache::NullStore.new
@etags = false
end
end
end
7 changes: 4 additions & 3 deletions lib/cache_crispies/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ module Controller
def cache_render(serializer, cacheable, options = {})
plan = CacheCrispies::Plan.new(serializer, cacheable, options)

# TODO: It would probably be good to add configuration to etiher
# enable or disable this
response.weak_etag = plan.etag
if CacheCrispies.config.etags?
response.weak_etag = plan.etag
end

serializer_json =
if plan.collection?
# TODO: try to cache the whole collection here
cacheable.map do |one_cacheable|
plan.cache { serializer.new(one_cacheable, options).as_json }
end
Expand Down
2 changes: 1 addition & 1 deletion lib/cache_crispies/plan.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def cache_key
# @return whatever the provided block returns
def cache
if cache?
Rails.cache.fetch(cache_key) { yield }
CacheCrispies.cache.fetch(cache_key) { yield }
else
yield
end
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion spec/base_spec.rb → spec/cache_crispies/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def id
describe '.cache_key_base' do
let(:nested_serializer_digest) { 'nutrition-serializer-digest' }
let(:serializer_file_path) {
File.expand_path('fixtures/test_serializer.rb', __dir__)
File.expand_path('../fixtures/test_serializer.rb', __dir__)
}
let(:serializer_file_digest) {
Digest::MD5.file(serializer_file_path).to_s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def models.cache_key() end

it "doesn't cache the results" do
expect(CacheCrispies::Plan).to_not receive(:new)
expect(Rails).to_not receive :cache
expect(CacheCrispies).to_not receive :cache
expect(subject.as_json).to eq [ {name: name1}, {name: name2} ]
end
end
Expand All @@ -45,7 +45,7 @@ def models.cache_key() end

it "doesn't cache the results" do
expect(CacheCrispies::Plan).to_not receive(:new)
expect(Rails).to_not receive :cache
expect(CacheCrispies).to_not receive :cache
expect(subject.as_json).to eq [ {name: name1}, {name: name2} ]
end
end
Expand All @@ -55,13 +55,13 @@ def models.cache_key() end
it 'caches the results' do
expect(CacheCrispies::Plan).to receive(:new).with(
serializer, model1, options
).and_return double(cache_key: 'cereal-key-1')
).and_return double('plan-dbl-1', cache_key: 'cereal-key-1')

expect(CacheCrispies::Plan).to receive(:new).with(
serializer, model2, options
).and_return double(cache_key: 'cereal-key-2')
).and_return double('plan-dbl-2', cache_key: 'cereal-key-2')

expect(Rails).to receive_message_chain(:cache, :fetch_multi).with(
expect(CacheCrispies).to receive_message_chain(:cache, :fetch_multi).with(
%w[cereal-key-1 cereal-key-2]
).and_yield('cereal-key-1').and_return(name: name1)
.and_yield('cereal-key-2').and_return(name: name2)
Expand All @@ -70,4 +70,4 @@ def models.cache_key() end
end
end
end
end
end
File renamed without changes.
42 changes: 42 additions & 0 deletions spec/cache_crispies/configuration_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
require 'spec_helper'

describe CacheCrispies::Configuration do
describe '#cache_store' do
context 'when Rails.cache is defined' do
let(:cache_double) { double }
before { expect(Rails).to receive(:cache).and_return cache_double }

it 'is Rails.cache by default' do
expect(subject.cache_store).to be cache_double
end
end

context 'when Rails.cache is nil' do
before { expect(Rails).to receive(:cache).and_return nil }

it 'is NullStore by default' do
expect(subject.cache_store).to be_kind_of ActiveSupport::Cache::NullStore
end
end

it 'can be changed' do
cache_store = ActiveSupport::Cache::NullStore.new

expect {
subject.cache_store = cache_store
}.to change { subject.cache_store }.to cache_store
end
end

describe '#etags' do
it 'is false by default' do
expect(subject.etags).to be false
end

it 'can be changed' do
expect {
subject.etags = true
}.to change { subject.etags }.to true
end
end
end
91 changes: 91 additions & 0 deletions spec/cache_crispies/controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
require 'spec_helper'
require 'active_model'
require 'action_controller'

describe CacheCrispies::Controller do
class Cereal
include ActiveModel::Model
attr_accessor :name
end

class CerealController < ActionController::Base
include CacheCrispies::Controller
end

class CerealSerializerForController < CacheCrispies::Base
serialize :name

def self.key
'cereal'
end
end

let(:cereal_names) { ['Count Chocula', 'Eyeholes'] }
let(:collection) { cereal_names.map { |name| Cereal.new(name: name) } }

subject { CerealController.new }

describe '#cache_render' do
let(:etags) { false }
let(:single_json) { { cereal: { name: cereal_names.first } }.to_json }
let(:collection_json) {
{ cereals: cereal_names.map { |name| { name: name } } }.to_json
}

before do
expect(
CacheCrispies
).to receive_message_chain(:config, :etags?).and_return etags
end

context 'with etags disabled' do
it 'does not set etags' do
expect(subject).to receive(:render).with json: collection_json
expect(subject.response).to_not receive(:weak_etag=)

subject.cache_render CerealSerializerForController, collection
end
end

context 'with etags enabled' do
let(:etags) { true }

it 'sets etags' do
expect(subject).to receive(:render).with json: collection_json
expect_any_instance_of(
CacheCrispies::Plan
).to receive(:etag).and_return 'test-etag'
expect(subject).to receive_message_chain(:response, :weak_etag=).with 'test-etag'

subject.cache_render CerealSerializerForController, collection
end
end

it 'renders a json collection' do
expect(subject).to receive(:render).with json: collection_json

subject.cache_render CerealSerializerForController, collection
end

it 'renders a single json object' do
expect(subject).to receive(:render).with json: single_json

subject.cache_render CerealSerializerForController, collection.first
end

context 'with a status: option' do
it 'passes the status option to the Rails render call' do
expect(subject).to receive(:render).with(
json: single_json,
status: 418
)

subject.cache_render(
CerealSerializerForController,
collection.first,
status: 418
)
end
end
end
end
File renamed without changes.
File renamed without changes.
6 changes: 3 additions & 3 deletions spec/plan_spec.rb → spec/cache_crispies/plan_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def self.cache_key_addons(options)

let(:serializer) { CerealSerializerForPlan }
let(:serializer_file_path) {
File.expand_path('fixtures/test_serializer.rb', __dir__)
File.expand_path('../fixtures/test_serializer.rb', __dir__)
}
let(:model_cache_key) { 'model-cache-key' }
let(:model) { OpenStruct.new(name: 'Sugar Smacks', cache_key: model_cache_key) }
Expand Down Expand Up @@ -133,7 +133,7 @@ def self.cache_key_addons(options)
describe '#cache' do
context 'when the plan is not cacheable' do
it "doesn't cache the results" do
expect(Rails).to_not receive(:cache)
expect(CacheCrispies).to_not receive(:cache)
subject.cache {}
end
end
Expand All @@ -142,7 +142,7 @@ def self.cache_key_addons(options)
it "doesn't cache the results" do
expect(subject).to receive(:cache?).and_return true
expect(subject).to receive(:cache_key).and_return 'bar'
expect(Rails).to receive_message_chain(:cache, :fetch).with('bar')
expect(CacheCrispies).to receive_message_chain(:cache, :fetch).with('bar')
subject.cache {}
end
end
Expand Down
27 changes: 27 additions & 0 deletions spec/cache_crispies_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require 'spec_helper'

describe CacheCrispies do
after { described_class.config.reset! }

describe '.configure and .config' do
it 'allows setting values' do
expect {
described_class.configure do |conf|
conf.etags = true
end
}.to change { described_class.config.etags }.from(false).to true
end
end

describe '.cache' do
it 'delegates to config.cache_store' do
cache_store = ActiveSupport::Cache::NullStore.new

expect {
described_class.configure do |conf|
conf.cache_store = cache_store
end
}.to change { described_class.cache }.to cache_store
end
end
end
Loading

0 comments on commit 7d2a3ba

Please sign in to comment.