Class: Common::Collection

Inherits:
Object
  • Object
show all
Extended by:
ActiveModel::Naming, Forwardable
Includes:
ActiveModel::Serialization
Defined in:
lib/common/models/collection.rb

Overview

Wrapper for collection to keep aggregates

Constant Summary collapse

CACHE_NAMESPACE =
'common_collection'
CACHE_DEFAULT_TTL =

default to 1 hour

3600
OPERATIONS_MAP =
{
  'eq' => '==',
  'lteq' => '<=',
  'gteq' => '>=',
  'not_eq' => '!=',
  'match' => 'match'
}.with_indifferent_access.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(klass = Array, data: [], metadata: {}, errors: {}, cache_key: nil) ⇒ Collection

Returns a new instance of Collection.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/common/models/collection.rb', line 33

def initialize(klass = Array, data: [], metadata: {}, errors: {}, cache_key: nil)
  data = Array.wrap(data) # If data is passed in as nil, wrap it as an empty array
  @type = klass
  @attributes = data
  @metadata = 
  @errors = errors
  @cache_key = cache_key

  (@data = data) && return if defined?(::WillPaginate::Collection) && data.is_a?(WillPaginate::Collection)

  @data = data.collect do |element|
    element.is_a?(Hash) ? klass.new(element) : element
  end
end

Instance Attribute Details

#attributesObject (readonly) Also known as: to_h, to_hash

Returns the value of attribute attributes.



16
17
18
# File 'lib/common/models/collection.rb', line 16

def attributes
  @attributes
end

#dataObject Also known as: members

Returns the value of attribute data.



17
18
19
# File 'lib/common/models/collection.rb', line 17

def data
  @data
end

#errorsObject

Returns the value of attribute errors.



17
18
19
# File 'lib/common/models/collection.rb', line 17

def errors
  @errors
end

#metadataObject

Returns the value of attribute metadata.



17
18
19
# File 'lib/common/models/collection.rb', line 17

def 
  @metadata
end

#typeObject

Returns the value of attribute type.



17
18
19
# File 'lib/common/models/collection.rb', line 17

def type
  @type
end

Class Method Details

.bust(cache_keys) ⇒ Object



79
80
81
82
# File 'lib/common/models/collection.rb', line 79

def self.bust(cache_keys)
  cache_keys = Array.wrap(cache_keys)
  cache_keys.map { |cache_key| redis_namespace.del(cache_key) }
end

.cache(json_hash, cache_key, ttl) ⇒ Object



74
75
76
77
# File 'lib/common/models/collection.rb', line 74

def self.cache(json_hash, cache_key, ttl)
  redis_namespace.set(cache_key, json_hash)
  redis_namespace.expire(cache_key, ttl)
end

.fetch(klass, cache_key: nil, ttl: CACHE_DEFAULT_TTL) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/common/models/collection.rb', line 56

def self.fetch(klass, cache_key: nil, ttl: CACHE_DEFAULT_TTL)
  raise 'No Block Given' unless block_given?

  if cache_key
    json_string = redis_namespace.get(cache_key)
    if json_string.nil?
      collection = new(klass, **yield.merge(cache_key:))
      cache(collection.serialize, cache_key, ttl)
      collection
    else
      json_hash = Oj.load(json_string)
      new(klass, **json_hash.merge('cache_key' => cache_key).symbolize_keys)
    end
  else
    new(klass, **yield)
  end
end

.redis_namespaceObject



48
49
50
# File 'lib/common/models/collection.rb', line 48

def self.redis_namespace
  @redis_namespace ||= Redis::Namespace.new(CACHE_NAMESPACE, redis: $redis)
end

Instance Method Details

#bustObject



84
85
86
# File 'lib/common/models/collection.rb', line 84

def bust
  self.class.bust(@cache_key) if cached?
end

#cached?Boolean

Returns:

  • (Boolean)


88
89
90
# File 'lib/common/models/collection.rb', line 88

def cached?
  @cache_key.present?
end

#convert_fields_to_ordered_hash(fields) ⇒ Object (private)



209
210
211
212
213
214
215
216
217
218
# File 'lib/common/models/collection.rb', line 209

def convert_fields_to_ordered_hash(fields)
  fields.each_with_object({}) do |field, hash|
    if field.start_with?('-')
      field = field[1..]
      hash[field] = 'DESC'
    else
      hash[field] = 'ASC'
    end
  end
end

#find_by(filter = {}) ⇒ Object



96
97
98
99
100
101
# File 'lib/common/models/collection.rb', line 96

def find_by(filter = {})
  verify_filter_keys!(filter)
  result = @data.select { |item| finder(item, filter) }
   = @metadata.merge(filter:)
  Collection.new(type, data: result, metadata:, errors:)
end

#find_first_by(filter = {}) ⇒ Object



103
104
105
106
107
108
109
110
# File 'lib/common/models/collection.rb', line 103

def find_first_by(filter = {})
  verify_filter_keys!(filter)
  result = @data.detect { |item| finder(item, filter) }
  return nil if result.nil?

  result. = 
  result
end

#finder(object, filter) ⇒ Object (private)

rubocop:disable Metrics/MethodLength



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/common/models/collection.rb', line 143

def finder(object, filter)
  filter.to_hash.all? do |attribute, predicates|
    actual_value = object.send(attribute)
    predicates.all? do |operator, expected_value|
      valid_operation = type.filterable_attributes[attribute].include?(operator.to_s)
      raise Common::Exceptions::FilterNotAllowed, "#{operator} for #{attribute}" unless valid_operation

      op = OPERATIONS_MAP.fetch(operator)

      parsed_value = expected_value.try(:split, ',') || expected_value

      results = Array.wrap(parsed_value).collect do |item|
        mock_comparator_object.send("#{attribute}=", item)

        if op == 'match'
          actual_value.downcase.include?(item.downcase)
        else
          actual_value.send(op, mock_comparator_object.send(attribute))
        end
      end

      results.any?
    end
  end
rescue => e
  raise e if e.is_a?(Common::Exceptions::BaseError)

  raise Common::Exceptions::InvalidFiltersSyntax.new(nil, detail: 'The syntax for your filters is invalid')
end

#mock_comparator_objectObject (private)



138
139
140
# File 'lib/common/models/collection.rb', line 138

def mock_comparator_object
  @mock_comparator_object ||= type.new
end

#paginate(page: nil, per_page: nil) ⇒ Object



124
125
126
127
128
129
130
# File 'lib/common/models/collection.rb', line 124

def paginate(page: nil, per_page: nil)
  page = page.try(:to_i) || 1
  max_per_page = type.max_per_page || 100
  per_page = [per_page.try(:to_i) || type.per_page || 10, max_per_page].min
  collection = paginator(page, per_page)
  Collection.new(type, data: collection, metadata: .merge(pagination_meta(page, per_page)), errors:)
end

#pagination_meta(page, per_page) ⇒ Object (private)



186
187
188
189
190
# File 'lib/common/models/collection.rb', line 186

def pagination_meta(page, per_page)
  total_entries = @data.size
  total_pages = total_entries.zero? ? 1 : (total_entries / per_page.to_f).ceil
  { pagination: { current_page: page, per_page:, total_pages:, total_entries: } }
end

#paginator(page, per_page) ⇒ Object (private)

rubocop:enable Metrics/MethodLength



174
175
176
177
178
179
180
181
182
183
184
# File 'lib/common/models/collection.rb', line 174

def paginator(page, per_page)
  if defined?(::WillPaginate::Collection)
    WillPaginate::Collection.create(page, per_page, @data.length) do |pager|
      raise Common::Exceptions::InvalidPaginationParams.new({ page:, per_page: }) if pager.out_of_bounds?

      pager.replace @data[pager.offset, pager.per_page]
    end
  else
    @data[((page - 1) * per_page)...(page * per_page)]
  end
end

#redis_namespaceObject



52
53
54
# File 'lib/common/models/collection.rb', line 52

def redis_namespace
  @redis_namespace ||= self.class.redis_namespace
end

#serializeObject



132
133
134
# File 'lib/common/models/collection.rb', line 132

def serialize
  { data:, metadata:, errors: }.to_json
end

#sort(sort_params) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
# File 'lib/common/models/collection.rb', line 112

def sort(sort_params)
  fields = sort_fields(sort_params || type.default_sort)
  result = @data.sort_by do |item|
    fields.map do |k, v|
      v == 'ASC' ? Ascending.new(item.send(k)) : Descending.new(item.send(k))
    end
  end

   = @metadata.merge(sort: fields)
  Collection.new(type, data: result, metadata:, errors:)
end

#sort_fields(params) ⇒ Object (private)



192
193
194
195
196
197
198
# File 'lib/common/models/collection.rb', line 192

def sort_fields(params)
  params = Array.wrap(params)
  not_allowed = params.select { |p| sort_type_allowed?(p) }.join(', ')
  raise Common::Exceptions::InvalidSortCriteria.new(type.name, not_allowed) unless not_allowed.empty?

  convert_fields_to_ordered_hash(params)
end

#sort_type_allowed?(sort_param) ⇒ Boolean (private)

Returns:

  • (Boolean)


200
201
202
# File 'lib/common/models/collection.rb', line 200

def sort_type_allowed?(sort_param)
  type.sortable_attributes.exclude?(sort_param.delete('-'))
end

#ttlObject



92
93
94
# File 'lib/common/models/collection.rb', line 92

def ttl
  @cache_key.present? ? redis_namespace.ttl(@cache_key) : nil
end

#verify_filter_keys!(filter) ⇒ Object (private)



204
205
206
207
# File 'lib/common/models/collection.rb', line 204

def verify_filter_keys!(filter)
  failed_attributes = (filter.keys.map(&:to_s) - type.filterable_attributes.keys).join(', ')
  raise Common::Exceptions::FilterNotAllowed, failed_attributes unless failed_attributes.empty?
end