Add enumerable attribute support to Model::Jbuilder::BuilderExpansion with unit tests
This commit is contained in:
parent
8bf52d2579
commit
e4e162c4ab
8 changed files with 286 additions and 41 deletions
|
@ -5,15 +5,98 @@
|
|||
module Model::Jbuilder
|
||||
extend ActiveSupport::Concern
|
||||
class_methods do
|
||||
def add_builder_expansion(*args, **kwargs)
|
||||
builder_expansions.add_builder_expansion(*args, **kwargs)
|
||||
#
|
||||
# Builder expansions address a common issue when using Jbuilder for generated JSON for object events work with `#init_builder`
|
||||
# In some situations, a model may reference another object but, depending on the situation, may not want to expand that object
|
||||
#
|
||||
# As an example, consider an hypothetical Supporter class with a reference to a Nonprofit
|
||||
#
|
||||
# class Supporter < ApplicationRecord
|
||||
# belongs_to :nonprofit
|
||||
# belongs_to :group
|
||||
# def to_builder(*expand)
|
||||
# init_builder(*expand) do
|
||||
# # build you JBuilder object
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# When generating the the Jbuilder object, you may want to expand the nonprofit object or in other situations not expand it. To handle this
|
||||
# you would need to write the following code:
|
||||
# `
|
||||
# if expand.include? :nonprofit
|
||||
# json.nonprofit nonprofit.to_builder.attributes!
|
||||
# else
|
||||
# json.nonprofit nonprofit.to_id
|
||||
# end
|
||||
# `
|
||||
# You would have to write the same code for `group`. As your number of expandable attributes increase, your code gets filled with boilerplate code.
|
||||
#
|
||||
# `add_builder_expansion` addresses this by autocreating this code in to_builder.
|
||||
# For example, if you want nonprofit to be expandable as in the nonprofit json attribute, and group into the group json attribute. You only need to write:
|
||||
# `add_builder_expansion :nonprofit, :group`
|
||||
# You can put as many expandable attributes there as you'd like.
|
||||
#
|
||||
# On the other hand, let's say you want to make group expandable BUT you want to assign it to the "supporter_group" json attribute. To do that, you
|
||||
# pass in the attribute you want to be expandable along with the "json_attribute" method key set to 'supporter_group':
|
||||
# `add_builder_expansion :group, json_attribute: 'supporter_group'`
|
||||
#
|
||||
# For enumerable attributes (like a has_many or an array), there are two ways you may want to include them into your json output. If it's a set of simple values
|
||||
# like, an array of strings or numbers, you may want to want to the array output as-is. For example, let's say you have a array of tags which are strings. You may want
|
||||
# it to be output like so:
|
||||
# ````
|
||||
# tags: [ 'founders circle', 'large donor']
|
||||
# ```
|
||||
# We call these `:flat` enumerable attribtes. Assuming the supporter class before has a tags attribute, you would add the tags builder_expansion using:
|
||||
# ```
|
||||
# add_builder_expansion :tags, enum_type: :flat
|
||||
# ```
|
||||
#
|
||||
# On the other hand, you may want to have an array of other Jbuilder created objects. Let's say your supporter has many groups. You may want these attributes expanded in
|
||||
# one of two ways: expanded or unexpanded.
|
||||
#
|
||||
# For expanded, you would receive have something like this in your output:
|
||||
#
|
||||
# {
|
||||
# # ... some other parts of the supporter json
|
||||
# groups: [{ id: 546, name: 'group name 1'}, {id: 235, name: 'group name 2'}]
|
||||
# }
|
||||
#
|
||||
# However, for unexpanded, you'd just want the ids:
|
||||
#
|
||||
# {
|
||||
# # ... some other parts of the supporter json
|
||||
# groups: [546, 235]
|
||||
# }
|
||||
#
|
||||
# For this type of builder expansion, you would use:
|
||||
# ```
|
||||
# add_builder expansion :groups, enum_type: :expandable
|
||||
# ```
|
||||
#
|
||||
# @param [Symbol] *attributes the attributes you'd like to make expandable. If you want to set options, there should only be a single attribute here.
|
||||
# @param **options options for configuring the builder expansion. The options right now are:
|
||||
# - `json_attribute`: the json attribute name in the outputted Jbuilder. Defaults to the attribute name.
|
||||
# - `enum_type`: the type of enumerable for the attribute. pass :flat if the enumerable attribute is flat, or :expandable if it's expandable. ANy other value
|
||||
# including the default of nil, means the attribute is not enumerable.
|
||||
#
|
||||
def add_builder_expansion(*attributes, **options)
|
||||
builder_expansions.add_builder_expansion(*attributes, **options)
|
||||
end
|
||||
|
||||
def builder_expansions
|
||||
#
|
||||
# A set of all the builder expansions configured for a class
|
||||
#
|
||||
# @return [Array] the builder expansions
|
||||
#
|
||||
def builder_expansions
|
||||
@builder_expansions ||= BuilderExpansionSet.new
|
||||
end
|
||||
end
|
||||
|
||||
def to_id
|
||||
id
|
||||
end
|
||||
|
||||
class BuilderExpansionSet < Set
|
||||
def add_builder_expansion(*args, **kwargs)
|
||||
|
@ -44,37 +127,59 @@ module Model::Jbuilder
|
|||
|
||||
class BuilderExpansion
|
||||
include ActiveModel::AttributeAssignment
|
||||
attr_accessor :key, :json_attrib, :to_id,
|
||||
:to_expand, :if, :unless, :on_else, :to_attrib
|
||||
attr_accessor :key, :json_attribute, :enum_type
|
||||
|
||||
def initialize(new_attributes)
|
||||
assign_attributes(new_attributes)
|
||||
end
|
||||
def enumerable?
|
||||
expandable_enum? || flat_enum?
|
||||
end
|
||||
|
||||
def json_attrib
|
||||
@json_attrib || key
|
||||
def expandable_enum?
|
||||
enum_type == :expandable
|
||||
end
|
||||
|
||||
def flat_enum?
|
||||
enum_type == :flat
|
||||
end
|
||||
|
||||
def json_attribute
|
||||
(@json_attribute || key).to_s
|
||||
end
|
||||
|
||||
def to_id
|
||||
if @to_id
|
||||
return @to_id
|
||||
elsif @to_attrib
|
||||
return -> (model, be=self) { to_attrib.(model).id }
|
||||
else
|
||||
return ->(model,be=self) { model.send(be.key).id}
|
||||
end
|
||||
return ->(model,be=self) {
|
||||
value = be.get_attribute_value model
|
||||
if be.expandable_enum?
|
||||
value&.map{|i| i&.to_id}
|
||||
elsif be.flat_enum?
|
||||
value
|
||||
else
|
||||
value&.to_id
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def to_expand
|
||||
if @to_expand
|
||||
return @to_expand
|
||||
elsif @to_attrib
|
||||
return -> (model, be=self) { to_attrib.(model).to_builder }
|
||||
else
|
||||
return ->(model,be=self) { model.send(be.key).to_builder}
|
||||
end
|
||||
def to_builder
|
||||
return ->(model,be=self) {
|
||||
value = be.get_attribute_value model
|
||||
if be.expandable_enum?
|
||||
value&.map{|i| i&.to_builder}
|
||||
elsif be.flat_enum?
|
||||
value
|
||||
else
|
||||
value&.to_builder
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
def get_attribute_value(model)
|
||||
if model.respond_to? key
|
||||
model.send(key)
|
||||
else
|
||||
raise ActiveModel::MissingAttributeError, "missing attribute: #{key}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -86,9 +191,9 @@ module Model::Jbuilder
|
|||
json.object self.class.name.underscore
|
||||
builder_expansions.keys.each do |k|
|
||||
if expand.include? k
|
||||
json.set! builder_expansions.get_by_key(k).json_attrib, builder_expansions.get_by_key(k).to_expand.(self)
|
||||
json.set! builder_expansions.get_by_key(k).json_attribute, builder_expansions.get_by_key(k).to_builder.(self)
|
||||
else
|
||||
json.set! builder_expansions.get_by_key(k).json_attrib, builder_expansions.get_by_key(k).to_id.(self)
|
||||
json.set! builder_expansions.get_by_key(k).json_attribute, builder_expansions.get_by_key(k).to_id.(self)
|
||||
end
|
||||
end
|
||||
yield(json)
|
||||
|
|
|
@ -13,7 +13,7 @@ module Model::TrxAssignable
|
|||
add_builder_expansion :nonprofit, :supporter
|
||||
|
||||
add_builder_expansion :trx,
|
||||
json_attrib: :transaction
|
||||
json_attribute: :transaction
|
||||
|
||||
has_one :transaction_assignment, as: :assignable
|
||||
has_one :trx, through: :transaction_assignment
|
||||
|
|
|
@ -10,7 +10,7 @@ class ModernCampaignGift < ApplicationRecord
|
|||
|
||||
add_builder_expansion :nonprofit, :supporter, :campaign, :campaign_gift_option, :campaign_gift_purchase
|
||||
add_builder_expansion :trx,
|
||||
json_attrib: :transaction
|
||||
json_attribute: :transaction
|
||||
|
||||
belongs_to :campaign_gift_purchase
|
||||
belongs_to :legacy_campaign_gift, class_name: 'CampaignGift', foreign_key: :campaign_gift_id, inverse_of: :modern_campaign_gift
|
||||
|
|
|
@ -18,10 +18,7 @@ class SupporterNote < ApplicationRecord
|
|||
validates :supporter_id, presence: true
|
||||
# TODO replace with Discard gem
|
||||
|
||||
add_builder_expansion :supporter, :nonprofit
|
||||
add_builder_expansion :user,
|
||||
to_id: ->(model) { model.user&.id},
|
||||
to_expand: ->(model) { model.user&.to_builder}
|
||||
add_builder_expansion :supporter, :nonprofit, :user
|
||||
|
||||
define_model_callbacks :discard
|
||||
|
||||
|
|
|
@ -6,11 +6,8 @@ class TicketPurchase < ApplicationRecord
|
|||
include Model::TrxAssignable
|
||||
setup_houid :tktpur
|
||||
|
||||
add_builder_expansion :event
|
||||
add_builder_expansion :event, :event_discount
|
||||
|
||||
add_builder_expansion :event_discount,
|
||||
to_id: -> (model) { model.event_discount&.id },
|
||||
to_expand: -> (model) { model.event_discount&.to_builder }
|
||||
|
||||
before_create :set_original_discount
|
||||
|
||||
|
|
|
@ -21,11 +21,7 @@ class TicketToLegacyTicket < ApplicationRecord
|
|||
|
||||
setup_houid :tkt
|
||||
|
||||
add_builder_expansion :ticket_purchase, :ticket_level, :supporter, :event, :nonprofit
|
||||
|
||||
add_builder_expansion :event_discount,
|
||||
to_id: -> (model) { model.event_discount&.id},
|
||||
to_expand: -> (model) { model.event_discount&.to_builder}
|
||||
add_builder_expansion :ticket_purchase, :ticket_level, :supporter, :event, :nonprofit, :event_discount
|
||||
|
||||
def to_builder(*expand)
|
||||
init_builder(*expand) do |json|
|
||||
|
|
150
spec/models/concerns/model/jbuilder_spec.rb
Normal file
150
spec/models/concerns/model/jbuilder_spec.rb
Normal file
|
@ -0,0 +1,150 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later
|
||||
# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Model::Jbuilder do
|
||||
describe Model::Jbuilder::BuilderExpansion do
|
||||
let(:model) do
|
||||
stub_const('HasToBuilderAndToId', Struct.new(:to_id, :to_builder))
|
||||
|
||||
stub_const('ModelClass', Struct.new(:filled, :unfilled, :enumerable))
|
||||
|
||||
ModelClass.new(
|
||||
HasToBuilderAndToId.new('id_result', 'builder_result'),
|
||||
nil,
|
||||
[
|
||||
HasToBuilderAndToId.new('enumerable_id_result_1', 'enumerable_builder_result_1'),
|
||||
HasToBuilderAndToId.new('enumerable_id_result_2', 'enumerable_builder_result_2')
|
||||
]
|
||||
)
|
||||
end
|
||||
let(:filled_expansion) { described_class.new(key: :filled, json_attribute: 'filled_attrib') }
|
||||
let(:unfilled_expansion) { described_class.new(key: :unfilled) }
|
||||
let(:nonexistent_expansion) { described_class.new(key: :nonexistent) }
|
||||
let(:expandable_expansion) { described_class.new(key: :enumerable, enum_type: :expandable) }
|
||||
let(:flat_expansion) { described_class.new(key: :enumerable, enum_type: :flat) }
|
||||
|
||||
describe 'expansion where the attribute is filled' do
|
||||
subject { filled_expansion }
|
||||
|
||||
it {
|
||||
is_expected.to have_attributes(
|
||||
json_attribute: 'filled_attrib',
|
||||
enumerable?: false,
|
||||
flat_enum?: false,
|
||||
expandable_enum?: false
|
||||
)
|
||||
}
|
||||
|
||||
describe '#to_id' do
|
||||
subject { filled_expansion.to_id.call(model) }
|
||||
|
||||
it { is_expected.to eq 'id_result' }
|
||||
end
|
||||
|
||||
describe '#to_builder' do
|
||||
subject { filled_expansion.to_builder.call(model) }
|
||||
|
||||
it { is_expected.to eq 'builder_result' }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'expansion where the attribute is unfilled' do
|
||||
subject { unfilled_expansion }
|
||||
|
||||
it {
|
||||
is_expected.to have_attributes(
|
||||
json_attribute: 'unfilled',
|
||||
enumerable?: false,
|
||||
flat_enum?: false,
|
||||
expandable_enum?: false
|
||||
)
|
||||
}
|
||||
|
||||
describe '#to_id' do
|
||||
subject { unfilled_expansion.to_id.call(model) }
|
||||
|
||||
it { is_expected.to eq nil }
|
||||
end
|
||||
|
||||
describe '#to_builder' do
|
||||
subject { unfilled_expansion.to_builder.call(model) }
|
||||
|
||||
it { is_expected.to eq nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'expansion where the attribute is nonexistent' do
|
||||
subject { nonexistent_expansion }
|
||||
|
||||
it {
|
||||
is_expected.to have_attributes(
|
||||
json_attribute: 'nonexistent',
|
||||
enumerable?: false,
|
||||
flat_enum?: false,
|
||||
expandable_enum?: false
|
||||
)
|
||||
}
|
||||
|
||||
it '.to_id raises error' do
|
||||
expect { nonexistent_expansion.to_id.call(model) }.to raise_error ActiveModel::MissingAttributeError
|
||||
end
|
||||
|
||||
it '.to_builder raises error' do
|
||||
expect { nonexistent_expansion.to_builder.call(model) }.to raise_error ActiveModel::MissingAttributeError
|
||||
end
|
||||
end
|
||||
|
||||
describe 'expansion of expandable enumerable returns an enumerable' do
|
||||
subject { expandable_expansion }
|
||||
|
||||
it {
|
||||
is_expected.to have_attributes(
|
||||
json_attribute: 'enumerable',
|
||||
enumerable?: true,
|
||||
flat_enum?: false,
|
||||
expandable_enum?: true
|
||||
)
|
||||
}
|
||||
|
||||
describe '#to_id' do
|
||||
subject { expandable_expansion.to_id.call(model) }
|
||||
|
||||
it { is_expected.to match(%w[enumerable_id_result_1 enumerable_id_result_2]) }
|
||||
end
|
||||
|
||||
describe '#to_builder' do
|
||||
subject { expandable_expansion.to_builder.call(model) }
|
||||
|
||||
it { is_expected.to match(%w[enumerable_builder_result_1 enumerable_builder_result_2]) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'expansion of flat enumerable returns an enumerable' do
|
||||
subject { flat_expansion }
|
||||
|
||||
it {
|
||||
is_expected.to have_attributes(
|
||||
json_attribute: 'enumerable',
|
||||
enumerable?: true,
|
||||
flat_enum?: true,
|
||||
expandable_enum?: false
|
||||
)
|
||||
}
|
||||
|
||||
describe '#to_id' do
|
||||
subject { expandable_expansion.to_id.call(model) }
|
||||
|
||||
it { is_expected.to match(%w[enumerable_id_result_1 enumerable_id_result_2]) }
|
||||
end
|
||||
|
||||
describe '#to_builder' do
|
||||
subject { expandable_expansion.to_builder.call(model) }
|
||||
|
||||
it { is_expected.to match(%w[enumerable_builder_result_1 enumerable_builder_result_2]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@ RSpec.describe Transaction, type: :model do
|
|||
describe 'to_builder' do
|
||||
subject { supporter.transactions.create(amount: 1000).to_builder.attributes!}
|
||||
it 'will create a proper builder result' do
|
||||
expect(subject).to match({
|
||||
is_expected.to match({
|
||||
'id' => match('trx_[a-zA-Z0-9]{22}'),
|
||||
'nonprofit' => nonprofit.id,
|
||||
'supporter' => supporter.id,
|
||||
|
|
Loading…
Reference in a new issue