Skip to content

Commit

Permalink
Merge pull request #58 from nametoolong/direct-render
Browse files Browse the repository at this point in the history
Direct JSON rendering with Oj::StringWriter
  • Loading branch information
adamcrown authored Jul 2, 2023
2 parents 14a0a7d + 8cabe62 commit 4fbd4f1
Show file tree
Hide file tree
Showing 9 changed files with 429 additions and 18 deletions.
1 change: 1 addition & 0 deletions lib/cache_crispies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module CacheCrispies
autoload :Optional, 'cache_crispies/optional'
autoload :Configuration, 'cache_crispies/configuration'
autoload :HashBuilder, 'cache_crispies/hash_builder'
autoload :JsonBuilder, 'cache_crispies/json_builder'
autoload :Memoizer, 'cache_crispies/memoizer'
autoload :Controller, 'cache_crispies/controller'
autoload :Plan, 'cache_crispies/plan'
Expand Down
51 changes: 43 additions & 8 deletions lib/cache_crispies/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,29 @@ def initialize(
# @raise [InvalidCoercionType] when an invalid argument is passed in the
# to: argument
def value_for(target, options)
value =
if block?
block.call(target, options)
elsif through?
target.public_send(through)&.public_send(method_name)
else
target.public_send(method_name)
end
value = retrieve_value(target, options)

serializer ? serialize(value, options) : coerce(value)
end

# Writes a key-value pair of the attribute for the given target object
# and options into the given Oj::StringWriter
#
# @param json_writer [Oj::StringWriter] any Oj::StringWriter instance
# @param target [Object] typically ActiveRecord::Base, but could be anything
# @param options [Hash] any optional values from the serializer instance
# @raise [InvalidCoercionType] when an invalid argument is passed in the
# to: argument
def write_to_json(json_writer, target, options)
value = retrieve_value(target, options)

if serializer
write_serialized_value(json_writer, value, options)
else
json_writer.push_value(coerce(value), key.to_s)
end
end

private

def through?
Expand All @@ -85,6 +96,16 @@ def block?
!block.nil?
end

def retrieve_value(target, options)
if block?
block.call(target, options)
elsif through?
target.public_send(through)&.public_send(method_name)
else
target.public_send(method_name)
end
end

# Here we'll render the attribute with a given serializer and attempt to
# cache the results for better cache reusability
def serialize(value, options)
Expand All @@ -99,6 +120,20 @@ def serialize(value, options)
end
end

def write_serialized_value(json_writer, value, options)
plan = CacheCrispies::Plan.new(
serializer, value, collection: collection, **options
)

json_writer.push_key(key.to_s) if key

if key && plan.collection?
Collection.new(value, serializer, options).write_to_json(json_writer)
else
JsonBuilder.new(serializer.new(value, options)).call(json_writer, flat: !key)
end
end

def coerce(value)
return value if coerce_to.nil?

Expand Down
19 changes: 19 additions & 0 deletions lib/cache_crispies/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ def as_json
HashBuilder.new(self).call
end

# Renders the serializer instance to an Oj::StringWriter instance
#
# @return [Oj::StringWriter] an Oj::StringWriter instance with the
# serialized content
def write_to_json(json_writer = nil)
json_writer ||= Oj::StringWriter.new(mode: :rails)

JsonBuilder.new(self).call(json_writer)

json_writer
end

# Get or set whether or not this serializer class should allow caching of
# results. It returns false by default, but can be overridden in child
# classes. Calling the method with an argument will set the value, calling
Expand Down Expand Up @@ -193,6 +205,13 @@ def self.file_hashes
).uniq.sort
end

def self.attributes_by_nesting
@attributes_by_nesting ||= (
attributes.sort_by(&:nesting).group_by(&:nesting)
)
end
delegate :attributes_by_nesting, to: :class

private

def self.file_hash
Expand Down
16 changes: 16 additions & 0 deletions lib/cache_crispies/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ def as_json
end
end

# Renders the collection to an Oj::StringWriter instance without caching
#
# @return [Oj::StringWriter] an Oj::StringWriter instance with the
# serialized content
def write_to_json(json_writer = nil)
json_writer ||= Oj::StringWriter.new(mode: :rails)
json_writer.push_array

collection.each do |model|
serializer.new(model, options).write_to_json(json_writer)
end

json_writer.pop
json_writer
end

private

attr_reader :collection, :serializer, :options
Expand Down
21 changes: 11 additions & 10 deletions lib/cache_crispies/hash_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def initialize(serializer)
#
# @return [Hash]
def call
return unless @serializer.model
return if @serializer.model.nil?

hash = {}

Expand All @@ -42,7 +42,7 @@ def call
hash
end

private
protected

attr_reader :serializer, :condition_results

Expand All @@ -56,18 +56,19 @@ def show?(attribute)
end
end

def value_for(attribute)
def target_for(attribute)
meth = attribute.method_name

target =
if meth != :itself && serializer.respond_to?(meth)
serializer
else
serializer.model
end
if serializer.respond_to?(meth) && meth != :itself
serializer
else
serializer.model
end
end

def value_for(attribute)
# TODO: rescue NoMethodErrors here with something more telling
attribute.value_for(target, serializer.options)
attribute.value_for(target_for(attribute), serializer.options)
end
end
end
69 changes: 69 additions & 0 deletions lib/cache_crispies/json_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

module CacheCrispies
# Builds out a JSON string directly with Oj::StringWriter
class JsonBuilder < HashBuilder
# Builds the JSON string with Oj::StringWriter
#
# @return [Oj::StringWriter]
def call(json_writer, flat: false)
if @serializer.model.nil?
json_writer.push_value(nil)
return
end

json_writer.push_object unless flat

last_nesting = []

serializer.attributes_by_nesting.each do |nesting, attributes|
prefix_length = common_prefix_length(last_nesting, nesting)

(last_nesting.length - prefix_length).times do
json_writer.pop
end

nesting[prefix_length..-1].each do |key|
json_writer.push_object(key.to_s)
end

attributes.each do |attrib|
write_attribute(json_writer, attrib) if show?(attrib)
end

last_nesting = nesting
end

last_nesting.each do
json_writer.pop
end

json_writer.pop unless flat

json_writer
end

protected

def write_attribute(json_writer, attribute)
# TODO: rescue NoMethodErrors here with something more telling
attribute.write_to_json(
json_writer,
target_for(attribute),
serializer.options
)
end

private

def common_prefix_length(array1, array2)
shorter_length = [array1.length, array2.length].min

(0...shorter_length).each do |i|
return i if array1[i] != array2[i]
end

shorter_length
end
end
end
44 changes: 44 additions & 0 deletions spec/cache_crispies/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,50 @@ class CacheCrispiesEngineTestSerializer < CacheCrispiesTestSerializer
end
end

describe '#write_to_json' do
let(:recovered_hash) { Oj.compat_load(subject.write_to_json.to_s, symbol_keys: true) }

it 'serializes to an Oj::StringWriter' do
expect(subject.write_to_json).to be_kind_of(Oj::StringWriter)

expect(recovered_hash).to eq(
id: '42',
name: 'Cookie Crisp',
company: 'General Mills',
nested: {
nested_again: {
deeply_nested: 'TRUE'
}
},
nutrition_info: {
calories: 1000
},
organic: true,
parent_company: 'Disney probably'
)
end

context 'when nutrition_info is nil' do
before { model.nutrition_info = nil }

it 'serializes to an Oj::StringWriter' do
expect(recovered_hash).to eq(
id: '42',
name: 'Cookie Crisp',
company: 'General Mills',
nested: {
nested_again: {
deeply_nested: 'TRUE'
}
},
nutrition_info: nil,
organic: true,
parent_company: 'Disney probably'
)
end
end
end

describe '.key' do
it 'underscores the demodulized class name by default' do
expect(serializer.key).to eq :cache_crispies_test
Expand Down
8 changes: 8 additions & 0 deletions spec/cache_crispies/collection_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,12 @@ def models.cache_key() end
end
end
end

describe '#write_to_json' do
let(:recovered_hash) { Oj.compat_load(subject.write_to_json.to_s, symbol_keys: true) }

it 'serializes to an Oj::StringWriter' do
expect(recovered_hash).to eq [{ name: name1 }, { name: name2 }]
end
end
end
Loading

0 comments on commit 4fbd4f1

Please sign in to comment.