Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] Support for nested associations defined at callsite #409

Closed
1 task done
jesseduffield opened this issue Mar 29, 2024 · 3 comments
Closed
1 task done

Comments

@jesseduffield
Copy link

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe

ActiveRecord lets you preload nested associations e.g.:

Blah.joins(:foo, bar: [:baz, :bez], bam: { bop: :boop })

I would like something similar with blueprinter, where by default associations are not included in the result, unless specified at the callsite.

Is this already possible, and if so, how might I go about doing it?

Describe the feature you'd like to see implemented

I want to be able to do something like this:

FooBlueprint.render_as_hash(foo, associations: { bar: [:baz, :bez], bam: { bop: :boop } })

In order to do this, I suspect I need some ability to take the options hash and go one level deeper each time we traverse an association, so that if I'm in the BarBlueprint, the options hash becomes { associations: [:baz, bez] }. But to my knowledge the options hash stays the same no matter how nested the blueprint is.

Thanks!

Describe alternatives you've considered

No response

Additional context

No response

@sandstrom
Copy link

sandstrom commented Apr 3, 2024

I think the increased ability to customize serializers would make this easier.

See this issue, on extractor configurability: #404


That said, it's already possible today, with some hacking around.

This is a dump of some experimentation I did earlier, where the stack is a way of keeping track of your location during traversal.

You should be able to take the code below and make some modifications to it, to get what you're asking for today.

# Invoke with this:
# InputBlueprint.render({ :user => User.all.first }, selected_keys: ['user.first_name', 'user.groups.name'], :cache => {})
# Example output:
#  "{\"private\":{\"exchange_rates\":{}},\"user\":{\"first_name\":\"Master\",\"groups\":[{\"name\":\"Kontroll\"}]}}"

def explode_string(str)
  parts = str.split('.')
  parts.map.with_index { |_, index| parts[0..index].join('.') }
end

Blueprinter.configure do |config|
  config.if = ->(field_name, obj, options) {
    # puts "global-if for field #{field_name}"
    # puts "global-if: #{options}"

    if stack = options[:stack]
      stack_key = stack.join('.')
      full_key = "#{stack_key}.#{field_name}"

      if selected_keys = options[:selected_keys]
        # the only keys that may be relevant, are those that start with the current stack
        relevant_keys = selected_keys.find_all { |key| key.start_with?(stack_key) }

        exploded_keys = relevant_keys.flat_map { |key| explode_string(key) }

        exploded_keys.include?(full_key)
      else # no selected keys, include all of them
        # puts "no selected keys"
        true
      end
    else # no stack exist (root node)
      # puts "no stack"
      true
    end
  }
end

class MyAssociationExtractor < Blueprinter::AssociationExtractor
  def extract(association_name, object, local_options, options = {})
    # puts "AssociationExtractor #{association_name} local_options: #{local_options}"
    # puts "AssociationExtractor #{association_name} options: #{options}"
    local_options[:stack] = [] unless local_options[:stack]
    local_options[:stack] << association_name.to_s

    super
  ensure
    local_options[:stack].pop
  end
end

class CacheExtractor < Blueprinter::Extractor
  def extract(field_name, object, local_options, options = {})
    # puts "local_options: #{local_options}"
    # puts "options: #{options}"

    local_options[:mut] = 1
    options[:mut] = 2

    if cache = local_options[:cache]
      object_cache_key = object.id.to_s

      if cache.key?(object_cache_key)
        if cache[object_cache_key].key?(field_name)
          puts 'cache hit!' # FIXME remove
          return cache[object_cache_key][field_name] # cache hit
        end
      else
        puts 'cache miss!'
        cache[object_cache_key] = {}
      end

      val = object.send(field_name) # cache miss
      cache[object_cache_key][field_name] = val
      val
    else
      puts 'no cache!'
      object.send(field_name)
      # super # see if this works
    end
  end
end

class GroupBlueprint < Blueprinter::Base
  identifier :id

  fields :name
  fields :internal_identifier
end

class UserBlueprint < Blueprinter::Base
  identifier :id

  fields :first_name, :last_name

  field :primary_email, extractor: CacheExtractor, made_up_option: 'hello_from_field_option'
  field :roles, extractor: CacheExtractor

  association :groups, blueprint: GroupBlueprint, extractor: MyAssociationExtractor
end

class InputBlueprint < Blueprinter::Base
  # identifier :id

  association :user, blueprint: UserBlueprint, extractor: MyAssociationExtractor

  field :private do |input, options|
    {
      :exchange_rates => {}, # input.active_expenses…
    }
  end
end

Copy link

github-actions bot commented Jun 3, 2024

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Jun 10, 2024
@jesseduffield
Copy link
Author

Sorry for the late reply, thanks for that example @sandstrom !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants