Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/jsonapi_compliable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
require "jsonapi_compliable/scope/filterable"
require "jsonapi_compliable/scope/default_filter"
require "jsonapi_compliable/scope/filter"
require "jsonapi_compliable/stats/dsl"
require "jsonapi_compliable/stats/payload"
require "jsonapi_compliable/util/include_params"
require "jsonapi_compliable/util/field_params"
require "jsonapi_compliable/util/scoping"
require "jsonapi_compliable/util/pagination"

require 'jsonapi_compliable/railtie' if defined?(::Rails)

Expand Down
15 changes: 9 additions & 6 deletions lib/jsonapi_compliable/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module Base

included do
class_attribute :_jsonapi_compliable
attr_reader :_jsonapi_scoped
attr_reader :_jsonapi_scope

before_action :parse_fieldsets!
after_action :reset_scope_flag
Expand Down Expand Up @@ -36,13 +36,14 @@ def jsonapi_scope(scope,
scope = JsonapiCompliable::Scope::ExtraFields.new(self, scope).apply if extra_fields
scope = JsonapiCompliable::Scope::Sideload.new(self, scope).apply if includes
scope = JsonapiCompliable::Scope::Sort.new(self, scope).apply if sort
# This is set before pagination so it can be re-used for stats
@_jsonapi_scope = scope
scope = JsonapiCompliable::Scope::Paginate.new(self, scope).apply if paginate
@_jsonapi_scoped = true
scope
end

def reset_scope_flag
@_jsonapi_scoped = false
@_jsonapi_scope = nil
end

def parse_fieldsets!
Expand All @@ -51,14 +52,16 @@ def parse_fieldsets!
end

def render_ams(scope, opts = {})
scope = jsonapi_scope(scope) if Util::Scoping.apply?(self, scope, opts.delete(:scope))
scoped = Util::Scoping.apply?(self, scope, opts.delete(:scope)) ? jsonapi_scope(scope) : scope
options = default_ams_options
options[:include] = forced_includes || Util::IncludeParams.scrub(self)
options[:jsonapi] = scope
options[:jsonapi] = JsonapiCompliable::Util::Pagination.zero?(params) ? [] : scoped
options[:fields] = Util::FieldParams.fieldset(params, :fields) if params[:fields]
options[:extra_fields] = Util::FieldParams.fieldset(params, :extra_fields) if params[:extra_fields]

options[:meta] ||= {}
options.merge!(opts)
options[:meta][:stats] = Stats::Payload.new(self, scoped).generate if params[:stats]

render(options)
end

Expand Down
15 changes: 15 additions & 0 deletions lib/jsonapi_compliable/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class DSL
:extra_fields,
:filters,
:sorting,
:stats,
:pagination

def initialize
Expand All @@ -19,6 +20,7 @@ def copy
instance.extra_fields = extra_fields.deep_dup
instance.sorting = sorting.deep_dup
instance.pagination = pagination.deep_dup
instance.stats = stats.deep_dup
instance
end

Expand All @@ -27,6 +29,7 @@ def clear!
@filters = {}
@default_filters = {}
@extra_fields = {}
@stats = {}
@sorting = nil
@pagination = nil
end
Expand All @@ -50,6 +53,12 @@ def allow_filter(name, *args, &blk)
}
end

def allow_stat(symbol_or_hash, &blk)
dsl = Stats::DSL.new(symbol_or_hash)
dsl.instance_eval(&blk) if blk
@stats[dsl.name] = dsl
end

def default_filter(name, &blk)
@default_filters[name.to_sym] = {
filter: blk
Expand All @@ -71,5 +80,11 @@ def extra_field(field, &blk)
proc: blk
}
end

def stat(attribute, calculation)
stats_dsl = @stats[attribute] || @stats[attribute.to_sym]
raise Errors::StatNotFound.new(attribute, calculation) unless stats_dsl
stats_dsl.calculation(calculation)
end
end
end
21 changes: 21 additions & 0 deletions lib/jsonapi_compliable/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,26 @@ def message
"Requested page size #{@size} is greater than max supported size #{@max}"
end
end

class StatNotFound < StandardError
def initialize(attribute, calculation)
@attribute = attribute
@calculation = calculation
end

def message
"No stat configured for calculation #{pretty(@calculation)} on attribute #{pretty(@attribute)}"
end

private

def pretty(input)
if input.is_a?(Symbol)
":#{input}"
else
"'#{input}'"
end
end
end
end
end
54 changes: 54 additions & 0 deletions lib/jsonapi_compliable/stats/dsl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module JsonapiCompliable
module Stats
class DSL
attr_reader :name, :calculations

def self.defaults
{
count: ->(scope, attr) { scope.count },
average: ->(scope, attr) { scope.average(attr).to_f },
sum: ->(scope, attr) { scope.sum(attr) },
maximum: ->(scope, attr) { scope.maximum(attr) },
minimum: ->(scope, attr) { scope.minimum(attr) }
}
end

def initialize(config)
config = { config => [] } if config.is_a?(Symbol)

@calculations = {}
@name = config.keys.first
Array(config.values.first).each { |c| send(:"#{c}!") }
end

def method_missing(meth, *args, &blk)
@calculations[meth] = blk
end

def calculation(name)
callable = @calculations[name] || @calculations[name.to_sym]
callable || raise(Errors::StatNotFound.new(@name, name))
end

def count!
@calculations[:count] = self.class.defaults[:count]
end

def sum!
@calculations[:sum] = self.class.defaults[:sum]
end

def average!
@calculations[:average] = self.class.defaults[:average]
end

def maximum!
@calculations[:maximum] = self.class.defaults[:maximum]
end

def minimum!
@calculations[:minimum] = self.class.defaults[:minimum]
end
end
end
end
34 changes: 34 additions & 0 deletions lib/jsonapi_compliable/stats/payload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module JsonapiCompliable
module Stats
class Payload
def initialize(controller, scope)
@dsl = controller._jsonapi_compliable
@directive = controller.params[:stats]
@scope = controller._jsonapi_scope || scope
end

def generate
{}.tap do |stats|
@directive.each_pair do |name, calculation|
stats[name] = {}

each_calculation(name, calculation) do |calc, function|
stats[name][calc] = function.call(@scope, name)
end
end
end
end

private

def each_calculation(name, calculation_string)
calculations = calculation_string.split(',').map(&:to_sym)

calculations.each do |calc|
function = @dsl.stat(name, calc)
yield calc, function
end
end
end
end
end
11 changes: 11 additions & 0 deletions lib/jsonapi_compliable/util/pagination.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module JsonapiCompliable
module Util
class Pagination
def self.zero?(params)
params = params[:page] || params['page'] || {}
size = params[:size] || params['size']
[0, '0'].include?(size)
end
end
end
end
4 changes: 2 additions & 2 deletions lib/jsonapi_compliable/util/scoping.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ module Util
class Scoping
def self.apply?(controller, object, force)
return false if force == false
return true if !controller._jsonapi_scoped && object.is_a?(ActiveRecord::Relation)
return true if controller._jsonapi_scope.nil? && object.is_a?(ActiveRecord::Relation)

already_scoped = !!controller._jsonapi_scoped
already_scoped = !!controller._jsonapi_scope
is_activerecord = object.is_a?(ActiveRecord::Base)
is_activerecord_array = object.is_a?(Array) && object[0].is_a?(ActiveRecord::Base)

Expand Down
49 changes: 49 additions & 0 deletions spec/dsl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
expect(copy.pagination).to eq(instance.pagination)
expect(copy.pagination.object_id).to_not eq(instance.pagination.object_id)
end

it 'copies stats' do
instance.stats = { foo: 'bar' }
expect(copy.stats).to eq(foo: 'bar')
expect(copy.stats.object_id).to_not eq(instance.stats.object_id)
end
end

describe '#clear' do
Expand All @@ -54,6 +60,7 @@
instance.filters = { foo: 'bar' }
instance.default_filters = { foo: 'bar' }
instance.extra_fields = { foo: 'bar' }
instance.stats = { foo: 'bar' }
instance.sorting = 'a'
instance.pagination = 'a'
end
Expand Down Expand Up @@ -93,5 +100,47 @@
instance.clear!
}.to change { instance.pagination }.to(nil)
end

it 'resets stats' do
expect {
instance.clear!
}.to change { instance.stats }.to({})
end
end

describe '#stat' do
let(:avg_proc) { proc { |scope, attr| 1 } }

before do
dsl = JsonapiCompliable::Stats::DSL.new(:myattr)
dsl.average(&avg_proc)
instance.stats = { myattr: dsl }
end

context 'when passing strings' do
it 'returns the corresponding proc' do
expect(instance.stat('myattr', 'average')).to eq(avg_proc)
end
end

context 'when passing symbols' do
it 'returns the corresponding proc' do
expect(instance.stat(:myattr, :average)).to eq(avg_proc)
end
end

context 'when no corresponding attribute' do
it 'raises error' do
expect { instance.stat(:foo, 'average') }
.to raise_error(JsonapiCompliable::Errors::StatNotFound, "No stat configured for calculation 'average' on attribute :foo")
end
end

context 'when no corresponding calculation' do
it 'raises error' do
expect { instance.stat('myattr', :median) }
.to raise_error(JsonapiCompliable::Errors::StatNotFound, "No stat configured for calculation :median on attribute :myattr")
end
end
end
end
5 changes: 3 additions & 2 deletions spec/jsonapi_compliable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,11 @@ def index
end

it 'resets scope flag after action' do
controller.instance_variable_set(:@_jsonapi_scope, 'a')
expect {
get :index
}.to change { controller.instance_variable_get(:@_jsonapi_scoped) }
.from(nil).to(false)
}.to change { controller.instance_variable_get(:@_jsonapi_scope) }
.from('a').to(nil)
end

context 'when passing scope: false' do
Expand Down
8 changes: 8 additions & 0 deletions spec/pagination_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ def index
expect(json_ids(true)).to eq([author3.id, author4.id])
end

# for metadata
context 'with page size 0' do
it 'should not respond with records, but still respond' do
get :index, params: { page: { size: 0 } }
expect(json_ids).to eq([])
end
end

context 'and a custom pagination function is given' do
before do
controller.class_eval do
Expand Down
Loading