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



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

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.



44
45
46
# File 'lib/brainstem/presenter.rb', line 44

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.



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

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.



66
67
68
# File 'lib/brainstem/presenter.rb', line 66

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

.reset!Object



60
61
62
63
# File 'lib/brainstem/presenter.rb', line 60

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.



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

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.



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

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.



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

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.



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

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.



148
149
# File 'lib/brainstem/presenter.rb', line 148

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.



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

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



77
78
79
80
81
82
# File 'lib/brainstem/presenter.rb', line 77

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.



88
89
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
# File 'lib/brainstem/presenter.rb', line 88

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.


73
74
75
# File 'lib/brainstem/presenter.rb', line 73

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

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



123
124
125
# File 'lib/brainstem/presenter.rb', line 123

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



243
244
245
# File 'lib/brainstem/presenter.rb', line 243

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