cache_crispies 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/lib/cache_crispies/attribute.rb +66 -0
- data/lib/cache_crispies/base.rb +110 -0
- data/lib/cache_crispies/collection.rb +43 -0
- data/lib/cache_crispies/condition.rb +30 -0
- data/lib/cache_crispies/controller.rb +26 -0
- data/lib/cache_crispies/hash_builder.rb +61 -0
- data/lib/cache_crispies/memoizer.rb +18 -0
- data/lib/cache_crispies/plan.rb +68 -0
- data/lib/cache_crispies/version.rb +3 -0
- data/lib/cache_crispies.rb +18 -0
- data/spec/attribute_spec.rb +159 -0
- data/spec/base_spec.rb +153 -0
- data/spec/collection_spec.rb +75 -0
- data/spec/condition_spec.rb +45 -0
- data/spec/controller_spec.rb +54 -0
- data/spec/fixtures/test_serializer.rb +2 -0
- data/spec/hash_builder_spec.rb +145 -0
- data/spec/memoizer_spec.rb +24 -0
- data/spec/plan_spec.rb +151 -0
- data/spec/spec_helper.rb +102 -0
- metadata +143 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bed703f9038261d6ff63e08465e7c0dccea1bcb6e2cb583de3ab154da562ef8f
|
4
|
+
data.tar.gz: ed55298237e61b9c6995a0194c61cd6ec221ea3cd62be176afa8842cb9d28c50
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e6897a7621e75851e7d9b2092674a44ed0ff42f328c61876b9de82dfc984c1eb2a7379dbe60f336659552be6445798063a1df83941fab55aeb0537a67fea5718
|
7
|
+
data.tar.gz: 0a9ac99db021a8e5ac1d06547d55c84e346aa550f020598f716993a5b783c2f2f8bcf002fda92d113f1864da2083f03719a5a240ae3e8dbebf3e24df01e1a543
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module CacheCrispies
|
2
|
+
class Attribute
|
3
|
+
class InvalidCoersionType < ArgumentError; end
|
4
|
+
|
5
|
+
def initialize(key, from: nil, with: nil, to: nil, nesting: [], conditions: [])
|
6
|
+
@key = key
|
7
|
+
@method_name = from || key || :itself
|
8
|
+
@serializer = with
|
9
|
+
@coerce_to = to
|
10
|
+
@nesting = Array(nesting)
|
11
|
+
@conditions = Array(conditions)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :method_name, :key, :serializer, :coerce_to, :nesting, :conditions
|
15
|
+
|
16
|
+
def value_for(model, options)
|
17
|
+
value = model.public_send(method_name)
|
18
|
+
|
19
|
+
serializer ? serialize(value, options) : coerce(value)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def serialize(value, options)
|
25
|
+
plan = CacheCrispies::Plan.new(serializer, value, options)
|
26
|
+
|
27
|
+
if value.respond_to?(:each)
|
28
|
+
plan.cache { Collection.new(value, serializer, options).as_json }
|
29
|
+
else
|
30
|
+
plan.cache { serializer.new(value, options).as_json }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def coerce(value)
|
35
|
+
return value if coerce_to.nil?
|
36
|
+
|
37
|
+
case coerce_to.to_s.to_sym
|
38
|
+
when :String
|
39
|
+
value.to_s
|
40
|
+
when :Integer
|
41
|
+
try_coerce_via_string(value, :to_i)
|
42
|
+
when :Float
|
43
|
+
try_coerce_via_string(value, :to_f)
|
44
|
+
when :BigDecimal
|
45
|
+
BigDecimal(value)
|
46
|
+
when :Array
|
47
|
+
Array(value)
|
48
|
+
when :Hash
|
49
|
+
value.respond_to?(:to_h) ? value.to_h : value.to_hash
|
50
|
+
when :bool, :boolean, :TrueClass, :FalseClass
|
51
|
+
!!value
|
52
|
+
else
|
53
|
+
raise(
|
54
|
+
InvalidCoersionType,
|
55
|
+
"#{coerce_to} has no registered coercion strategy"
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def try_coerce_via_string(value, method_name)
|
61
|
+
(
|
62
|
+
value.respond_to?(method_name) ? value : value.to_s
|
63
|
+
).public_send(method_name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
module CacheCrispies
|
5
|
+
class Base
|
6
|
+
attr_reader :model, :options
|
7
|
+
|
8
|
+
def initialize(model, options = {})
|
9
|
+
@model = model
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def as_json
|
14
|
+
HashBuilder.new(self).call
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.do_caching?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.key
|
22
|
+
to_s.demodulize.chomp('Serializer').underscore.to_sym
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.collection_key
|
26
|
+
return nil unless key
|
27
|
+
|
28
|
+
key.to_s.pluralize.to_sym
|
29
|
+
end
|
30
|
+
|
31
|
+
# Can be overridden in subclasses
|
32
|
+
# options: Hash of the same options that would be passed to the
|
33
|
+
# individual serializer instances
|
34
|
+
def self.cache_key_addons(_options = {})
|
35
|
+
[]
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.cache_key_base
|
39
|
+
# TODO: we may need to get a cache key from nested serializers as well :(
|
40
|
+
@cache_key_base ||= "#{self}-#{file_hash}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.attributes
|
44
|
+
@attributes || []
|
45
|
+
end
|
46
|
+
delegate :attributes, to: :class
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def self.file_hash
|
51
|
+
@file_hash ||= Digest::MD5.file(path).to_s
|
52
|
+
end
|
53
|
+
private_class_method :file_hash
|
54
|
+
|
55
|
+
def self.path
|
56
|
+
@path ||= begin
|
57
|
+
parts = %w[app serializers]
|
58
|
+
parts += to_s.deconstantize.split('::').map(&:underscore)
|
59
|
+
parts << "#{to_s.demodulize.underscore}.rb"
|
60
|
+
Rails.root.join(*parts)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
private_class_method :path
|
64
|
+
|
65
|
+
def self.nest_in(key, &block)
|
66
|
+
@nesting ||= []
|
67
|
+
@nesting << key
|
68
|
+
|
69
|
+
block.call
|
70
|
+
|
71
|
+
@nesting.pop
|
72
|
+
end
|
73
|
+
private_class_method :nest_in
|
74
|
+
|
75
|
+
def self.show_if(condition_proc, &block)
|
76
|
+
@conditions ||= []
|
77
|
+
@conditions << Condition.new(condition_proc)
|
78
|
+
|
79
|
+
block.call
|
80
|
+
|
81
|
+
@conditions.pop
|
82
|
+
end
|
83
|
+
private_class_method :show_if
|
84
|
+
|
85
|
+
def self.serialize(*attribute_names, from: nil, with: nil, to: nil)
|
86
|
+
@attributes ||= []
|
87
|
+
|
88
|
+
attribute_names.flatten.map { |att| att&.to_sym }.map do |attrib|
|
89
|
+
current_nesting = Array(@nesting).dup
|
90
|
+
current_conditions = Array(@conditions).dup
|
91
|
+
|
92
|
+
@attributes <<
|
93
|
+
Attribute.new(
|
94
|
+
attrib,
|
95
|
+
from: from,
|
96
|
+
with: with,
|
97
|
+
to: to,
|
98
|
+
nesting: current_nesting,
|
99
|
+
conditions: current_conditions
|
100
|
+
)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
private_class_method :serialize
|
104
|
+
|
105
|
+
def self.merge(attribute = nil, with: nil)
|
106
|
+
serialize(nil, from: attribute, with: with)
|
107
|
+
end
|
108
|
+
private_class_method :merge
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rails'
|
2
|
+
|
3
|
+
module CacheCrispies
|
4
|
+
class Collection
|
5
|
+
def initialize(collection, serializer, options = {})
|
6
|
+
@collection = collection
|
7
|
+
@serializer = serializer
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def as_json
|
12
|
+
if serializer.do_caching? && collection.respond_to?(:cache_key)
|
13
|
+
cached_json
|
14
|
+
else
|
15
|
+
uncached_json
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :collection, :serializer, :options
|
22
|
+
|
23
|
+
def uncached_json
|
24
|
+
collection.map do |model|
|
25
|
+
serializer.new(model, options).as_json
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def cached_json
|
30
|
+
models_by_cache_key = collection.each_with_object({}) do |model, hash|
|
31
|
+
plan = Plan.new(serializer, model, options)
|
32
|
+
|
33
|
+
hash[plan.cache_key] = model
|
34
|
+
end
|
35
|
+
|
36
|
+
Rails.cache.fetch_multi(models_by_cache_key.keys) do |cache_key|
|
37
|
+
model = models_by_cache_key[cache_key]
|
38
|
+
|
39
|
+
serializer.new(model, options).as_json
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module CacheCrispies
|
2
|
+
# Represents an instance of a conditional built by a show_if call
|
3
|
+
class Condition
|
4
|
+
def initialize(block)
|
5
|
+
@block = block
|
6
|
+
end
|
7
|
+
|
8
|
+
# Public: A system-wide unique ID used for memoizaiton
|
9
|
+
# Returns an Integer
|
10
|
+
def uid
|
11
|
+
# Just reusing the block's object_id seems to make sense
|
12
|
+
block.object_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def true_for?(model, options = {})
|
16
|
+
!!case block.arity
|
17
|
+
when 0
|
18
|
+
block.call
|
19
|
+
when 1
|
20
|
+
block.call(model)
|
21
|
+
else
|
22
|
+
block.call(model, options)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :block
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module CacheCrispies
|
2
|
+
module Controller
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
OJ_MODE = :rails
|
6
|
+
|
7
|
+
def cache_render(serializer, cacheable, options = {})
|
8
|
+
plan = CacheCrispies::Plan.new(serializer, cacheable, options)
|
9
|
+
|
10
|
+
# TODO: It would probably be good to add configuration to etiher
|
11
|
+
# enable or disable this
|
12
|
+
response.weak_etag = plan.etag
|
13
|
+
|
14
|
+
serializer_json =
|
15
|
+
if plan.collection?
|
16
|
+
cacheable.map do |one_cacheable|
|
17
|
+
plan.cache { serializer.new(one_cacheable, options).as_json }
|
18
|
+
end
|
19
|
+
else
|
20
|
+
plan.cache { serializer.new(cacheable, options).as_json }
|
21
|
+
end
|
22
|
+
|
23
|
+
render json: Oj.dump(plan.wrap(serializer_json), mode: OJ_MODE)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module CacheCrispies
|
2
|
+
class HashBuilder
|
3
|
+
def initialize(serializer)
|
4
|
+
@serializer = serializer
|
5
|
+
@condition_results = Memoizer.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def call
|
9
|
+
hash = {}
|
10
|
+
|
11
|
+
serializer.attributes.each do |attrib|
|
12
|
+
next unless show?(attrib)
|
13
|
+
|
14
|
+
deepest_hash = hash
|
15
|
+
|
16
|
+
attrib.nesting.each do |key|
|
17
|
+
deepest_hash[key] ||= {}
|
18
|
+
deepest_hash = deepest_hash[key]
|
19
|
+
end
|
20
|
+
|
21
|
+
value = value_for(attrib)
|
22
|
+
|
23
|
+
if attrib.key
|
24
|
+
deepest_hash[attrib.key] = value
|
25
|
+
else
|
26
|
+
deepest_hash.merge! value
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
hash
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :serializer, :condition_results
|
36
|
+
|
37
|
+
def show?(attribute)
|
38
|
+
# Memoize conditions so they aren't executed for each attribute in a
|
39
|
+
# show_if block
|
40
|
+
attribute.conditions.all? do |cond|
|
41
|
+
condition_results.fetch(cond.uid) do
|
42
|
+
cond.true_for?(serializer.model, serializer.options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def value_for(attribute)
|
48
|
+
meth = attribute.method_name
|
49
|
+
|
50
|
+
target =
|
51
|
+
if meth != :itself && serializer.respond_to?(meth)
|
52
|
+
serializer
|
53
|
+
else
|
54
|
+
serializer.model
|
55
|
+
end
|
56
|
+
|
57
|
+
# TODO: rescue NoMethodErrors here with something more telling
|
58
|
+
attribute.value_for(target, serializer.options)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module CacheCrispies
|
2
|
+
class Memoizer
|
3
|
+
def initialize
|
4
|
+
@cache = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def fetch(key, &_block)
|
8
|
+
# Avoid ||= because we need to memoize falsey values.
|
9
|
+
return cache[key] if cache.key?(key)
|
10
|
+
|
11
|
+
cache[key] = yield
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_reader :cache
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module CacheCrispies
|
2
|
+
class Plan
|
3
|
+
attr_reader :serializer, :cacheable, :options
|
4
|
+
|
5
|
+
def initialize(serializer, cacheable, options = {})
|
6
|
+
@serializer = serializer
|
7
|
+
@cacheable = cacheable
|
8
|
+
@key = options.delete(:key)
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def collection?
|
13
|
+
cacheable.respond_to?(:each)
|
14
|
+
end
|
15
|
+
|
16
|
+
def etag
|
17
|
+
Digest::MD5.hexdigest(cache_key)
|
18
|
+
end
|
19
|
+
|
20
|
+
def cache_key
|
21
|
+
@cache_key ||=
|
22
|
+
[
|
23
|
+
CACHE_KEY_PREFIX,
|
24
|
+
serializer.cache_key_base,
|
25
|
+
addons_key,
|
26
|
+
cacheable.cache_key
|
27
|
+
].flatten.compact.join(CACHE_KEY_SEPARATOR)
|
28
|
+
end
|
29
|
+
|
30
|
+
def cache
|
31
|
+
if cache?
|
32
|
+
Rails.cache.fetch(cache_key) { yield }
|
33
|
+
else
|
34
|
+
yield
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def wrap(json_hash)
|
39
|
+
return json_hash unless key?
|
40
|
+
|
41
|
+
{ key => json_hash }
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def key
|
47
|
+
return @key unless @key.nil?
|
48
|
+
|
49
|
+
(collection? ? serializer.collection_key : serializer.key)
|
50
|
+
end
|
51
|
+
|
52
|
+
def key?
|
53
|
+
!!key
|
54
|
+
end
|
55
|
+
|
56
|
+
def cache?
|
57
|
+
serializer.do_caching? && cacheable.respond_to?(:cache_key)
|
58
|
+
end
|
59
|
+
|
60
|
+
def addons_key
|
61
|
+
addons = serializer.cache_key_addons(options)
|
62
|
+
|
63
|
+
return nil if addons.compact.empty?
|
64
|
+
|
65
|
+
Digest::MD5.hexdigest(addons.join('|'))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'active_support/dependencies'
|
2
|
+
require 'oj'
|
3
|
+
|
4
|
+
module CacheCrispies
|
5
|
+
CACHE_KEY_PREFIX = 'cache-crispies'.freeze
|
6
|
+
CACHE_KEY_SEPARATOR = '+'.freeze
|
7
|
+
|
8
|
+
require 'cache_crispies/version'
|
9
|
+
|
10
|
+
autoload :Attribute, 'cache_crispies/attribute'
|
11
|
+
autoload :Base, 'cache_crispies/base'
|
12
|
+
autoload :Collection, 'cache_crispies/collection'
|
13
|
+
autoload :Condition, 'cache_crispies/condition'
|
14
|
+
autoload :HashBuilder, 'cache_crispies/hash_builder'
|
15
|
+
autoload :Memoizer, 'cache_crispies/memoizer'
|
16
|
+
autoload :Controller, 'cache_crispies/controller'
|
17
|
+
autoload :Plan, 'cache_crispies/plan'
|
18
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CacheCrispies::Attribute do
|
4
|
+
class NameSerializer < CacheCrispies::Base
|
5
|
+
serialize :spanish
|
6
|
+
end
|
7
|
+
|
8
|
+
class ToHashClass
|
9
|
+
def to_hash
|
10
|
+
{ portuguese: 'Capitão Crise' }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:key) { :name }
|
15
|
+
let(:from) { nil }
|
16
|
+
let(:with) { nil }
|
17
|
+
let(:to) { nil }
|
18
|
+
let(:nesting) { [] }
|
19
|
+
let(:conditions) { [] }
|
20
|
+
let(:instance) {
|
21
|
+
described_class.new(
|
22
|
+
key,
|
23
|
+
from: from,
|
24
|
+
with: with,
|
25
|
+
to: to,
|
26
|
+
nesting: nesting,
|
27
|
+
conditions: conditions
|
28
|
+
)
|
29
|
+
}
|
30
|
+
|
31
|
+
subject { instance }
|
32
|
+
|
33
|
+
describe '#value_for' do
|
34
|
+
let(:name) { "Cap'n Crunch" }
|
35
|
+
let(:model) { OpenStruct.new(name: name) }
|
36
|
+
let(:options) { {} }
|
37
|
+
|
38
|
+
subject { instance.value_for(model, options) }
|
39
|
+
|
40
|
+
it 'returns the value' do
|
41
|
+
expect(subject).to eq name
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'with a from: argument' do
|
45
|
+
let(:key) { :nombre }
|
46
|
+
let(:from) { :name }
|
47
|
+
|
48
|
+
it 'returns the value using the from: attribute' do
|
49
|
+
expect(subject).to eq name
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'with a with: argument' do
|
54
|
+
let(:spanish_name) { 'Capitán Crujido' }
|
55
|
+
let(:name) { OpenStruct.new(spanish: spanish_name)}
|
56
|
+
let(:with) { NameSerializer }
|
57
|
+
|
58
|
+
it 'returns the value using the from attribute' do
|
59
|
+
expect(subject).to eq spanish: spanish_name
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'with a to: argument' do
|
64
|
+
context 'when corecing to a String' do
|
65
|
+
let(:name) { 1138 }
|
66
|
+
let(:to) { String }
|
67
|
+
|
68
|
+
it 'returns a String' do
|
69
|
+
expect(subject).to eq '1138'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'when corecing to an Integer' do
|
74
|
+
let(:name) { '1138' }
|
75
|
+
let(:to) { Integer }
|
76
|
+
|
77
|
+
it 'returns a String' do
|
78
|
+
expect(subject).to eq 1138
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'when corecing to an Float' do
|
83
|
+
let(:name) { '1138' }
|
84
|
+
let(:to) { Float }
|
85
|
+
|
86
|
+
it 'returns a String' do
|
87
|
+
expect(subject).to eq 1138.0
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'when corecing to an BigDecimal' do
|
92
|
+
let(:name) { '1138' }
|
93
|
+
let(:to) { BigDecimal }
|
94
|
+
|
95
|
+
it 'returns a String' do
|
96
|
+
expect(subject).to eq BigDecimal(1138)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'when corecing to an Array' do
|
101
|
+
let(:name) { 1138 }
|
102
|
+
let(:to) { Array }
|
103
|
+
|
104
|
+
it 'returns an Array' do
|
105
|
+
expect(subject).to eq [1138]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'when corecing to a Hash' do
|
110
|
+
let(:to) { Hash }
|
111
|
+
|
112
|
+
context 'that responds to to_h' do
|
113
|
+
let(:french_name) { 'capitaine croquer' }
|
114
|
+
let(:name) { OpenStruct.new(french: french_name) }
|
115
|
+
|
116
|
+
it 'returns a Hash' do
|
117
|
+
expect(subject).to eq french: french_name
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context 'that responds to to_hash' do
|
122
|
+
let(:name) { ToHashClass.new }
|
123
|
+
|
124
|
+
it 'returns a Hash' do
|
125
|
+
expect(subject).to eq portuguese: 'Capitão Crise'
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context 'when corecing to an boolean' do
|
131
|
+
let(:to) { :boolean }
|
132
|
+
|
133
|
+
context 'when value is falsey' do
|
134
|
+
let(:name) { nil }
|
135
|
+
|
136
|
+
it 'returns false' do
|
137
|
+
expect(subject).to be false
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context 'when value is truthy' do
|
142
|
+
let(:name) { 'true' }
|
143
|
+
|
144
|
+
it 'returns true' do
|
145
|
+
expect(subject).to be true
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'when corercing to an invalid type' do
|
151
|
+
let(:to) { OpenStruct }
|
152
|
+
|
153
|
+
it 'raises an exception' do
|
154
|
+
expect { subject }.to raise_exception CacheCrispies::Attribute::InvalidCoersionType
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|