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

Extractor configurability #404

Open
1 task done
sandstrom opened this issue Mar 21, 2024 · 7 comments
Open
1 task done

Extractor configurability #404

sandstrom opened this issue Mar 21, 2024 · 7 comments

Comments

@sandstrom
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

Association extractor is useful to modify to e.g. add a circuit-breaker, implement caching and for a few other things. Right now it's somewhat complicated to override/modify.

Describe the feature you'd like to see implemented

Right now, there are two types of extractors: AssociationExtractor and AutoExtractor (along with some sub-extractors).

Their roles are somewhat intertwined (AssociationExtractor calls the AutoExtractor, for example), and only one of them can be modified globally.

I'd propose a stricter separation, splitting it up into (probably) three extractors:

  • FieldExtractor
  • AssociationExtractor
  • ValueExtractor (or DataExtractor)

The default ValueExtractor would encapsulate the logic currently existing in AutoExtractor, and I'd move the logic of deferring to block to field/association extractors. ValueExtractor could also be named something like DataExtractor instead.

Example

This is a rough sketch, in reality it would probably be slightly different.

class ValueExtractor < Extractor
  def extract(field_name, object, local_options, options = {})
    if object.is_a?(Hash)
      value = extract_hash(field_name, object, local_options, options)
    else
      value = extract_send(field_name, object, local_options, options)
    end
  end

  private

  def extract_hash(field_name, object, _local_options, _options = {})
    object[field_name]
  end

  def extract_send(field_name, object, _local_options, _options = {})
    object.public_send(field_name)
  end
end

class FieldExtractor < Extractor
  include EmptyTypes

  def initialize
    @extractor = Blueprinter.configuration.field_extractor_default.new # `ValueExtractor.new` by default
  end

  def extract(field_name, object, local_options, options = {})
    if options[:block]
      value = options[:block].call(object, local_options)
    else
      value = @value_extractor.extract(field_name, object, local_options, options)
    end

    value = @datetime_formatter.format(value, options)
    use_default_value?(value, options[:default_if]) ? default_value(options) : value
  end

  private

  def default_value(field_options)
    field_options.key?(:default) ? field_options.fetch(:default) : Blueprinter.configuration.field_default
  end
end

class AssociationExtractor < Extractor
  include EmptyTypes

  def initialize
    @extractor = Blueprinter.configuration.field_extractor_default.new # `ValueExtractor.new` by default
  end

  def extract(association_name, object, local_options, options = {})
    options_without_default = options.except(:default, :default_if)
    
    # Merge in assocation options hash
    local_options = local_options.merge(options[:options]) if options[:options].is_a?(Hash)

    if options[:block]
      value = options[:block].call(object, local_options)
    else
      value = @value_extractor.extract(association_name, object, local_options, options)
    end

    return default_value(options) if use_default_value?(value, options[:default_if])

    view = options[:view] || :default
    blueprint = association_blueprint(options[:blueprint], value)
    blueprint.prepare(value, view_name: view, local_options: local_options)
  end

  private

  def default_value(association_options)
    return association_options.fetch(:default) if association_options.key?(:default)

    Blueprinter.configuration.association_default
  end

  def association_blueprint(blueprint, value)
    blueprint.is_a?(Proc) ? blueprint.call(value) : blueprint
  end
end

Describe alternatives you've considered

No response

Additional context

No response

@sandstrom sandstrom mentioned this issue Mar 21, 2024
1 task
@sandstrom
Copy link
Author

Happy to discuss this in more detail, and elaborate more.

@sandstrom
Copy link
Author

Friendly ping @lessthanjacob @njbbaer @ritikesh

If you think this is a reasonable suggestion, let me know! If so, I'd propose these steps:

  • I'll flesh out the proposed change in some more detail (written, in this issue)
  • You get another chance to provide more feedback, and if you're happy we'll open a PR

For reference, there are also a few other issues that I've opened, where I'd also be happy to get your input.

Full list

@lessthanjacob
Copy link
Contributor

I think this would be a reasonable refactor.

@njbbaer @ritikesh any thoughts/concerns here?

@ritikesh
Copy link
Collaborator

Agreed, we are open to contributions @sandstrom.

@sandstrom
Copy link
Author

@lessthanjacob @ritikesh Thanks!

I'll have a chat with a colleague of mine, and we'll try to get back with a proposal.

Copy link

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.

@sandstrom
Copy link
Author

Still relevant!

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

3 participants