Class: Brainstem::Presenter Abstract

Inherits:
Object
  • Object
show all
Includes:
Concerns::PresenterDSL
Defined in:
lib/brainstem/presenter.rb,
lib/brainstem/time_classes.rb

Overview

This class is abstract.

Subclass and override #present to implement a presenter.

Constant Summary collapse

TIME_CLASSES =

This constant stores an array of classes that we will treat as times. Unfortunately, ActiveSupport::TimeWithZone does not descend from Time, so we put them into this array for later use.

[Time]

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Concerns::InheritableConfiguration

#configuration

Class Method Details

.merged_helper_classObject



50
51
52
53
54
55
56
57
58
59
60
# File 'lib/brainstem/presenter.rb', line 50

def self.merged_helper_class
  @helper_classes ||= {}

  @helper_classes[configuration[:helpers].to_a.map(&:object_id)] ||= begin
    Class.new.tap do |klass|
      (configuration[:helpers] || []).each do |helper|
        klass.send :include, helper
      end
    end
  end
end

.namespaceString

Return the second-to-last module in the name of this presenter, which Brainstem considers to be the ‘namespace’. E.g., Api::V1::FooPresenter has a namespace of “V1”.

Returns:

  • (String)

    The name of the second-to-last module containing this presenter.



46
47
48
# File 'lib/brainstem/presenter.rb', line 46

def self.namespace
  self.to_s.split("::")[-2].try(:downcase)
end

.possible_brainstem_keysObject

Returns the set of possible brainstem keys for the classes presented.

If the presenter specifies a key, that will be returned as the only member of the set.



36
37
38
39
40
# File 'lib/brainstem/presenter.rb', line 36

def self.possible_brainstem_keys
  @possible_brainstem_keys ||= begin
    Set.new(presents.map(&presenter_collection.method(:brainstem_key_for!)))
  end
end

.presents(*klasses) ⇒ Object

Accepts a list of classes that this specific presenter knows how to present. These are not inherited.

Parameters:

  • klasses (String, [String])

    Any number of names of classes this presenter presents.



17
18
19
20
21
22
23
24
25
26
27
# File 'lib/brainstem/presenter.rb', line 17

def self.presents(*klasses)
  @presents ||= []
  if klasses.length > 0
    if klasses.any? { |klass| klass.is_a?(String) || klass.is_a?(Symbol) }
      raise "Brainstem Presenter#presents now expects a Class instead of a class name"
    end
    @presents.concat(klasses).uniq!
    Brainstem.add_presenter_class(self, namespace, *klasses)
  end
  @presents
end

.reflections(klass) ⇒ Object

In Rails 4.2, ActiveRecord::Base#reflections started being keyed by strings instead of symbols.



68
69
70
# File 'lib/brainstem/presenter.rb', line 68

def self.reflections(klass)
  klass.reflections.each_with_object({}) { |(key, value), memo| memo[key.to_s] = value }
end

.reset!Object



62
63
64
65
# File 'lib/brainstem/presenter.rb', line 62

def self.reset!
  clear_configuration!
  @helper_classes = @presents = nil
end

Instance Method Details

#allowed_associations(is_only_query) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Determines which associations are valid for inclusion in the current context. Mostly just removes only-restricted associations when needed.

Returns:

  • (Hash)

    The associations that can be included.



141
142
143
144
145
146
147
# File 'lib/brainstem/presenter.rb', line 141

def allowed_associations(is_only_query)
  ActiveSupport::HashWithIndifferentAccess.new.tap do |associations|
    configuration[:associations].each do |name, association|
      associations[name] = association unless association.options[:restrict_to_only] && !is_only_query
    end
  end
end

#apply_filters_to_scope(scope, user_params, options) ⇒ Object

Given user params, build a hash of validated filter names to their unsanitized arguments.



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/brainstem/presenter.rb', line 188

def apply_filters_to_scope(scope, user_params, options)
  helper_instance = fresh_helper_instance

  requested_filters = extract_filters(user_params, options)
  requested_filters.each do |filter_name, filter_arg|
    filter_lambda = configuration[:filters][filter_name][:value]

    args_for_filter_lambda = [filter_arg]
    args_for_filter_lambda << requested_filters if configuration[:filters][filter_name][:include_params]

    if filter_lambda
      scope = helper_instance.instance_exec(scope, *args_for_filter_lambda, &filter_lambda)
    else
      scope = scope.send(filter_name, *args_for_filter_lambda)
    end
  end

  scope
end

#apply_ordering_to_scope(scope, user_params) ⇒ Object

Given user params, apply a validated sort order to the given scope.



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/brainstem/presenter.rb', line 209

def apply_ordering_to_scope(scope, user_params)
  sort_name, direction = calculate_sort_name_and_direction(user_params)
  order = configuration[:sort_orders].fetch(sort_name, {})[:value]

  ordered_scope = case order
    when Proc
      fresh_helper_instance.instance_exec(scope, direction, &order)
    when nil
      scope
    else
      scope.reorder(order.to_s + " " + direction)
  end

  fallback_deterministic_sort = assemble_primary_key_sort(scope)
  # Chain on a tiebreaker sort to ensure deterministic ordering of multiple pages of data

  if fallback_deterministic_sort
    ordered_scope.order(fallback_deterministic_sort)
  else
    ordered_scope
  end
end

#calculate_sort_name_and_direction(user_params = {}) ⇒ Object

Clean and validate a sort order and direction from user params.



250
251
252
253
254
255
256
257
258
259
# File 'lib/brainstem/presenter.rb', line 250

def calculate_sort_name_and_direction(user_params = {})
  default_column, default_direction = (configuration[:default_sort_order] || "updated_at:desc").split(":")
  sort_name, direction = user_params['order'].to_s.split(":")
  unless sort_name.present? && configuration[:sort_orders][sort_name]
    sort_name = default_column
    direction = default_direction
  end

  [sort_name, direction == 'desc' ? 'desc' : 'asc']
end

#custom_preload(models, requested_associations = []) ⇒ Object

Subclasses can define this if they wish. This method will be called by #group_present.



150
151
# File 'lib/brainstem/presenter.rb', line 150

def custom_preload(models, requested_associations = [])
end

#extract_filters(user_params, options = {}) ⇒ Object

Given user params, build a hash of validated filter names to their unsanitized arguments.



154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/brainstem/presenter.rb', line 154

def extract_filters(user_params, options = {})
  filters_hash = {}

  apply_default_filters = options.fetch(:apply_default_filters) { true }

  configuration[:filters].each do |filter_name, filter_options|
    user_value = format_filter_value(user_params[filter_name])

    filter_arg = apply_default_filters && user_value.nil? ? filter_options[:default] : user_value
    filters_hash[filter_name] = filter_arg unless filter_arg.nil?
  end

  filters_hash
end

#get_query_strategyObject



79
80
81
82
83
84
# File 'lib/brainstem/presenter.rb', line 79

def get_query_strategy
  if configuration.has_key? :query_strategy
    strat = configuration[:query_strategy]
    strat.respond_to?(:call) ? fresh_helper_instance.instance_exec(&strat) : strat
  end
end

#group_present(models, requested_associations = [], options = {}) ⇒ Object

Calls #custom_preload and then presents all models.



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/brainstem/presenter.rb', line 90

def group_present(models, requested_associations = [], options = {})
  association_objects_by_name = requested_associations.each_with_object({}) do |assoc_name, memo|
    memo[assoc_name.to_s] = configuration[:associations][assoc_name] if configuration[:associations][assoc_name]
  end

  # It's slightly ugly, but more efficient if we pre-load everything we
  # need and pass it through.
  context = {
    conditional_cache:            { request: {} },
    fields:                       configuration[:fields],
    conditionals:                 configuration[:conditionals],
    associations:                 configuration[:associations],
    reflections:                  reflections_for_model(models.first),
    association_objects_by_name:  association_objects_by_name,
    optional_fields:              options[:optional_fields] || [],
    models:                       models,
    lookup:                       empty_lookup_cache(configuration[:fields].keys, association_objects_by_name.keys)
  }

  sanitized_association_names = association_objects_by_name.values.map(&:method_name)
  preload_associations! models, sanitized_association_names, context[:reflections]

  # Legacy: Overridable for custom preload behavior.
  custom_preload(models, association_objects_by_name.keys)

  models.map do |model|
    context[:conditional_cache][:model] = {}
    context[:helper_instance] = fresh_helper_instance
    result = present_fields(model, context, context[:fields])
    load_associations!(model, result, context, options)
    add_id!(model, result)
    datetimes_to_json(result)
  end
end

#present(model) ⇒ Object

Deprecated.


75
76
77
# File 'lib/brainstem/presenter.rb', line 75

def present(model)
  raise "#present is now deprecated"
end

#present_model(model, requested_associations = [], options = {}) ⇒ Object



125
126
127
# File 'lib/brainstem/presenter.rb', line 125

def present_model(model, requested_associations = [], options = {})
  group_present([model], requested_associations, options).first
end

#run_search(query, search_options) ⇒ Object

Execute the stored search block



245
246
247
# File 'lib/brainstem/presenter.rb', line 245

def run_search(query, search_options)
  fresh_helper_instance.instance_exec(query, search_options, &configuration[:search])
end