# 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 module Model::Jbuilder extend ActiveSupport::Concern class_methods do # # 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 # # A set of all the builder expansions configured for a class # # @return [Array] the builder expansions # def builder_expansions @builder_expansions ||= BuilderExpansionSet.new end def init_builder(model, *expand) JbuilderWithExpansions.new(model, *expand) do | json| json.(model, :id) json.object model.class.name.underscore yield(json) end end end def to_id id end class BuilderExpansionSet < Set def add_builder_expansion(*args, **kwargs) be = nil if args.any? || kwargs.any? if (args.count == 1 && kwargs.any?) be = BuilderExpansion.new(**{key: args[0]}.merge(kwargs)) add(be) else args.each do |a| be = BuilderExpansion.new(key: a) add(be) end end else raise ArgumentError end end def keys map{|i| i.key} end def get_by_key(key) select{|i| i.key == key}.first end end class BuilderExpansion include ActiveModel::AttributeAssignment attr_accessor :key, :json_attribute, :enum_type def initialize(new_attributes) assign_attributes(new_attributes) end def enumerable? expandable_enum? || flat_enum? end 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 return ->(model,be=self) { value = be.get_attribute_value model if be.expandable_enum? value&.map do |i| id_result = i&.to_id if ::Jbuilder === id_result id_result.attributes! else id_result end end elsif be.flat_enum? value else value&.to_id end } end def to_builder return ->(model,be=self) { value = be.get_attribute_value model if be.expandable_enum? value&.map{|i| i&.to_builder.attributes!} 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 def init_builder(*expand, &block) self.class.init_builder(self, *expand, &block) end class JbuilderWithExpansions < ::Jbuilder attr_reader :model, :expand delegate_missing_to :@jbuilder def initialize(model, *expand, &block) @model = model @expand = expand super(&block) end def add_builder_expansion( ... ) builder_expansions = BuilderExpansionSet.new builder_expansions.add_builder_expansion( ... ) builder_expansions.keys.each do |k| if expand.include? k set! builder_expansions.get_by_key(k).json_attribute, builder_expansions.get_by_key(k).to_builder.(model) else set! builder_expansions.get_by_key(k).json_attribute, builder_expansions.get_by_key(k).to_id.(model) end end end end end