cache_crispies 0.1.0
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.
- 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
|