compendium 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +15 -0
  2. data/README.md +70 -4
  3. data/Rakefile +4 -0
  4. data/app/assets/stylesheets/compendium/_metrics.css.scss +92 -0
  5. data/app/assets/stylesheets/compendium/options.css.scss +41 -0
  6. data/app/assets/stylesheets/compendium/report.css.scss +22 -0
  7. data/app/classes/compendium/presenters/base.rb +30 -0
  8. data/app/classes/compendium/presenters/chart.rb +31 -0
  9. data/app/classes/compendium/presenters/metric.rb +19 -0
  10. data/app/classes/compendium/presenters/option.rb +97 -0
  11. data/app/classes/compendium/presenters/query.rb +23 -0
  12. data/app/classes/compendium/presenters/settings/query.rb +22 -0
  13. data/app/classes/compendium/presenters/settings/table.rb +23 -0
  14. data/app/classes/compendium/presenters/table.rb +81 -0
  15. data/app/controllers/compendium/reports_controller.rb +52 -0
  16. data/app/helpers/compendium/reports_helper.rb +21 -0
  17. data/app/views/compendium/reports/run.haml +1 -0
  18. data/app/views/compendium/reports/setup.haml +14 -0
  19. data/compendium.gemspec +7 -1
  20. data/config/initializers/rails/active_record/connection_adapters/quoting.rb +14 -0
  21. data/config/initializers/ruby/numeric.rb +26 -0
  22. data/config/locales/en.yml +5 -0
  23. data/lib/compendium/abstract_chart_provider.rb +30 -0
  24. data/lib/compendium/chart_provider/amcharts.rb +20 -0
  25. data/lib/compendium/context_wrapper.rb +27 -0
  26. data/lib/compendium/dsl.rb +79 -0
  27. data/lib/compendium/engine/mount.rb +13 -0
  28. data/lib/compendium/engine.rb +8 -0
  29. data/lib/compendium/metric.rb +29 -0
  30. data/lib/compendium/open_hash.rb +68 -0
  31. data/lib/compendium/option.rb +37 -0
  32. data/lib/compendium/param_types.rb +91 -0
  33. data/lib/compendium/params.rb +40 -0
  34. data/lib/compendium/query.rb +94 -0
  35. data/lib/compendium/report.rb +56 -0
  36. data/lib/compendium/result_set.rb +24 -0
  37. data/lib/compendium/version.rb +1 -1
  38. data/lib/compendium.rb +46 -1
  39. data/spec/context_wrapper_spec.rb +71 -0
  40. data/spec/dsl_spec.rb +90 -0
  41. data/spec/metric_spec.rb +84 -0
  42. data/spec/option_spec.rb +12 -0
  43. data/spec/param_types_spec.rb +147 -0
  44. data/spec/params_spec.rb +28 -0
  45. data/spec/presenters/base_spec.rb +20 -0
  46. data/spec/presenters/option_spec.rb +49 -0
  47. data/spec/query_spec.rb +33 -0
  48. data/spec/report_spec.rb +93 -0
  49. data/spec/spec_helper.rb +1 -0
  50. metadata +135 -14
data/compendium.gemspec CHANGED
@@ -10,12 +10,18 @@ Gem::Specification.new do |gem|
10
10
  gem.email = ["dvandersluis@selfmgmt.com"]
11
11
  gem.description = %q{Ruby on Rails reporting framework}
12
12
  gem.summary = %q{Ruby on Rails reporting framework}
13
- gem.homepage = ""
13
+ gem.homepage = "https://github.com/dvandersluis/compendium"
14
+ gem.license = "MIT"
14
15
 
15
16
  gem.files = `git ls-files`.split($/)
16
17
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
19
  gem.require_paths = ["lib"]
19
20
 
21
+ gem.add_dependency 'rails', '>= 3.0.0'
22
+ gem.add_dependency 'sass-rails', '>= 3.0.0'
23
+ gem.add_dependency 'compass-rails', '>= 1.0.0'
24
+ gem.add_dependency 'collection_of', '>= 1.0.3'
25
+ gem.add_dependency 'inheritable_attr', '>= 1.0.0'
20
26
  gem.add_development_dependency 'rspec', '~> 2.0'
21
27
  end
@@ -0,0 +1,14 @@
1
+ # ActiveRecord doesn't know how to handle SimpleDelegators when creating SQL
2
+ # This means that when passing a SimpleDelegator (ie. Compendium::Param) into ActiveRecord::Base.find, it'll
3
+ # crash.
4
+ # Override AR::ConnectionAdapters::Quoting to forward a SimpleDelegator's object to be quoted.
5
+
6
+ module ActiveRecord::ConnectionAdapters::Quoting
7
+ def quote_with_simple_delegator(value, column = nil)
8
+ return value.quoted_id if value.respond_to?(:quoted_id)
9
+ value = value.__getobj__ if value.is_a?(SimpleDelegator)
10
+ quote_without_simple_delegator(value, column)
11
+ end
12
+
13
+ alias_method_chain :quote, :simple_delegator
14
+ end
@@ -0,0 +1,26 @@
1
+ # Monkey patch to determine if an object is numeric
2
+ # Only Numerics and Strings/Symbols that are representations of numbers are numeric
3
+
4
+ class Object
5
+ def numeric?
6
+ false
7
+ end
8
+ end
9
+
10
+ class String
11
+ def numeric?
12
+ !(self =~ /\A-?\d+(\.\d+)?\z|\A-?\.\d+\z/).nil?
13
+ end
14
+ end
15
+
16
+ class Symbol
17
+ def numeric?
18
+ to_s.numeric?
19
+ end
20
+ end
21
+
22
+ class Numeric
23
+ def numeric?
24
+ true
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ en:
2
+ compendium:
3
+ reports:
4
+ invalid_report: "Invalid report!"
5
+ generate_report: "Generate Report"
@@ -0,0 +1,30 @@
1
+ require 'active_support/core_ext/string/inflections'
2
+
3
+ module Compendium
4
+ # Abstract wrapper for rendering charts
5
+ # To add a new chart provider, #initialize and #render must be implemented
6
+ class AbstractChartProvider
7
+ attr_reader :chart
8
+
9
+ def initialize(type, data, &setup_proc)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ def render(template, container)
14
+ raise NotImplementedError
15
+ end
16
+
17
+ # As more chart providers are added, this method will have to be extended to find them
18
+ def self.find_chart_provider
19
+ if defined?(AmCharts)
20
+ :AmCharts
21
+ else
22
+ self.name.demodulize.to_sym
23
+ end
24
+ end
25
+ end
26
+
27
+ module ChartProvider
28
+ autoload :AmCharts, 'compendium/chart_provider/amcharts'
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ module Compendium
2
+ module ChartProvider
3
+ # Uses the amcharts.rb gem to provide charting
4
+ class AmCharts < Compendium::AbstractChartProvider
5
+ def initialize(type, data, &setup_proc)
6
+ @chart = chart_class(type).new(data, &setup_proc)
7
+ end
8
+
9
+ def render(template, container)
10
+ template.amchart(chart, container)
11
+ end
12
+
13
+ private
14
+
15
+ def chart_class(type)
16
+ ::AmCharts::Chart.const_get(type.to_s.titlecase)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ require 'delegate'
2
+
3
+ module Compendium
4
+ class ContextWrapper
5
+ def self.wrap(ctx, parent, params = nil, &block)
6
+ delegator = ::SimpleDelegator.new(parent)
7
+
8
+ delegator.define_singleton_method(:__context__) { ctx }
9
+
10
+ delegator.instance_eval do
11
+ def method_missing(name, *args, &block)
12
+ return __context__.__send__(name, *args, &block) if __context__.respond_to?(name)
13
+ super
14
+ end
15
+
16
+ def respond_to_missing?(name, include_private = false)
17
+ return true if __context__.respond_to?(name, include_private)
18
+ super
19
+ end
20
+ end
21
+
22
+ return delegator.instance_exec(params, &block) if block_given?
23
+
24
+ delegator
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,79 @@
1
+ require 'collection_of'
2
+ require 'inheritable_attr'
3
+ require 'compendium/option'
4
+ require 'active_support/core_ext/class/attribute'
5
+
6
+ module Compendium
7
+ module DSL
8
+ def self.extended(klass)
9
+ klass.inheritable_attr :queries, default: ::Collection[Query]
10
+ klass.inheritable_attr :options, default: {}
11
+ end
12
+
13
+ def query(name, opts = {}, &block)
14
+ define_query(name, opts, &block)
15
+ end
16
+ alias_method :chart, :query
17
+ alias_method :data, :query
18
+
19
+ def option(name, *args)
20
+ opts = args.extract_options!
21
+ type = args.shift
22
+
23
+ if options[name]
24
+ options[name].type = type if type
25
+ options[name].merge!(opts)
26
+ else
27
+ options[name] = Compendium::Option.new(opts.merge(name: name, type: type))
28
+ end
29
+ end
30
+
31
+ def metric(name, proc, opts = {})
32
+ raise ArgumentError, 'through option must be specified for metric' unless opts.key?(:through)
33
+
34
+ [opts.delete(:through)].flatten.each do |query|
35
+ raise ArgumentError, "query #{query} is not defined" unless queries.key?(query)
36
+ queries[query].add_metric(name, proc, opts)
37
+ end
38
+ end
39
+
40
+ # Allow defined queries to be redefined by name, eg:
41
+ # query :main_query
42
+ # main_query { collect_records_here }
43
+ def method_missing(name, *args, &block)
44
+ if queries.keys.include?(name.to_sym)
45
+ query = queries[name.to_sym]
46
+ query.proc = block if block_given?
47
+ query.options = args.extract_options!
48
+ return query
49
+ end
50
+
51
+ super
52
+ end
53
+
54
+ def respond_to_missing?(name, *args)
55
+ return true if queries.keys.include?(name)
56
+ super
57
+ end
58
+
59
+ private
60
+
61
+ def define_query(name, opts, type = Query, &block)
62
+ name = name.to_sym
63
+ query = type.new(name, opts, block)
64
+
65
+ if opts.key?(:through)
66
+ through = [opts[:through]].flatten
67
+
68
+ through.each do |q|
69
+ raise ArgumentError, "query #{q} is not defined" unless self.queries.include?(q.to_sym)
70
+ end
71
+
72
+ query.through = through
73
+ end
74
+
75
+ metrics[name] = opts[:metric] if opts.key?(:metric)
76
+ queries << query
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,13 @@
1
+ module ActionDispatch
2
+ module Routing
3
+ class Mapper
4
+ def mount_compendium(options = {})
5
+ scope options[:at], controller: options.fetch(:controller, 'compendium/reports') do
6
+ get ':report_name', action: :setup, as: 'compendium_reports_setup'
7
+ post ':report_name', action: :run, as: 'compendium_reports_run'
8
+ root action: :index, as: 'compendium_reports_root'
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ require 'compendium/engine/mount'
2
+
3
+ module Compendium
4
+ if defined?(Rails)
5
+ class Engine < ::Rails::Engine
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,29 @@
1
+ module Compendium
2
+ Metric = Struct.new(:name, :query, :command, :options) do
3
+ attr_accessor :result
4
+
5
+ def initialize(*)
6
+ super
7
+ self.options ||= {}
8
+ end
9
+
10
+ def run(ctx, data)
11
+ self.result = if condition_failed?(ctx)
12
+ nil
13
+ else
14
+ command.is_a?(Symbol) ? ctx.send(command, data) : ctx.instance_exec(data, &command)
15
+ end
16
+ end
17
+
18
+ def ran?
19
+ !result.nil?
20
+ end
21
+ alias_method :has_ran?, :ran?
22
+
23
+ private
24
+
25
+ def condition_failed?(ctx)
26
+ (options.key?(:if) and !ctx.instance_exec(&options[:if])) or (options.key?(:unless) and ctx.instance_exec(&options[:unless]))
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,68 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+
3
+ module Compendium
4
+ class OpenHash < ::ActiveSupport::HashWithIndifferentAccess
5
+ class << self
6
+ def [](hash = {})
7
+ new(hash)
8
+ end
9
+ end
10
+
11
+ def dup
12
+ self.class.new(self)
13
+ end
14
+
15
+ protected
16
+
17
+ def convert_value(value)
18
+ if value.is_a? Hash
19
+ Params[value].tap do |oh|
20
+ oh.each do |k, v|
21
+ oh[k] = convert_value(v) if v.is_a? Hash
22
+ end
23
+ end
24
+ elsif value.is_a?(Array)
25
+ value.dup.replace(value.map { |e| convert_value(e) })
26
+ else
27
+ value
28
+ end
29
+ end
30
+
31
+ def method_missing(name, *args, &block)
32
+ method = name.to_s
33
+
34
+ case method
35
+ when %r{.=$}
36
+ super unless args.length == 1
37
+ return self[method[0...-1]] = args.first
38
+
39
+ when %r{.\?$}
40
+ super unless args.empty?
41
+ return self.key?(method[0...-1].to_sym)
42
+
43
+ when %r{^_.}
44
+ super unless args.empty?
45
+ return self[method[1..-1]] if self.key?(method[1..-1].to_sym)
46
+
47
+ else
48
+ return self[method] if key?(method) or !respond_to?(method)
49
+ end
50
+
51
+ super
52
+ end
53
+
54
+ def respond_to_missing?(name, include_private = false)
55
+ method = name.to_s
56
+
57
+ case method
58
+ when %r{.[=?]$}
59
+ return true if self.key?(method[0...-1])
60
+
61
+ when %r{^_.}
62
+ return true if self.key?(method[1..-1])
63
+ end
64
+
65
+ super
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,37 @@
1
+ require 'compendium/open_hash'
2
+ require 'active_support/string_inquirer'
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+ require 'active_support/core_ext/module/delegation'
5
+
6
+ module Compendium
7
+ class Option
8
+ attr_reader :name, :type, :default, :options
9
+
10
+ delegate :boolean?, :date?, :dropdown?, :radio?, :text?, to: :type
11
+ delegate :merge, :merge!, :[], to: :@options
12
+
13
+ def initialize(hash = {})
14
+ raise ArgumentError, "name must be provided" unless hash.key?(:name)
15
+
16
+ @name = hash.delete(:name).to_sym
17
+ @default = hash.delete(:default)
18
+ self.type = hash.delete(:type)
19
+ @options = hash.with_indifferent_access
20
+ end
21
+
22
+ def type=(type)
23
+ @type = ActiveSupport::StringInquirer.new(type.to_s)
24
+ end
25
+
26
+ def method_missing(name, *args, &block)
27
+ return options[name] if options.key?(name)
28
+ return options.key?(name[0...-1]) if name.to_s.end_with?('?')
29
+ super
30
+ end
31
+
32
+ def respond_to_missing?(name, include_private = false)
33
+ return true if options.key?(name)
34
+ super
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,91 @@
1
+ require_relative '../../config/initializers/ruby/numeric'
2
+ require 'delegate'
3
+
4
+ module Compendium
5
+ class Param < ::SimpleDelegator
6
+ def boolean?; false; end
7
+ def date?; false; end
8
+ def dropdown?; false; end
9
+ def radio?; false; end
10
+
11
+ def ==(other)
12
+ return true if (value == other rescue false)
13
+ super
14
+ end
15
+
16
+ # Need to explicitly delegate nil? to the object, otherwise it's always false
17
+ # This is because SimpleDelegator is a non-nil object, and it only forwards non-defined methods!
18
+ def nil?
19
+ __getobj__.nil?
20
+ end
21
+ end
22
+
23
+ class ParamWithChoices < Param
24
+ def initialize(obj, choices)
25
+ @choices = choices
26
+
27
+ if @choices.respond_to?(:call)
28
+ # If given a proc, defer determining values until later.
29
+ index = obj
30
+ else
31
+ index = obj.numeric? ? obj.to_i : @choices.index(obj)
32
+ raise IndexError if (!obj.nil? and index.nil?) or index.to_i.abs > @choices.length - 1
33
+ end
34
+
35
+ super(index || 0)
36
+ end
37
+
38
+ def value
39
+ @choices[self]
40
+ end
41
+ end
42
+
43
+ class BooleanParam < Param
44
+ def initialize(obj, *)
45
+ # If given 0, 1, or a version thereof (ie. "0"), pass it along
46
+ return super obj.to_i if obj.numeric? and (0..1).cover?(obj.to_i)
47
+ super !!obj ? 0 : 1
48
+ end
49
+
50
+ def boolean?
51
+ true
52
+ end
53
+
54
+ def value
55
+ [true, false][self]
56
+ end
57
+
58
+ # When negating a BooleanParam, use the value instead
59
+ def !
60
+ !value
61
+ end
62
+ end
63
+
64
+ class DateParam < Param
65
+ def initialize(obj, *)
66
+ if obj.respond_to?(:to_date)
67
+ obj = obj.to_date
68
+ else
69
+ obj = Date.parse(obj) rescue nil
70
+ end
71
+
72
+ super obj
73
+ end
74
+
75
+ def date?
76
+ true
77
+ end
78
+ end
79
+
80
+ class RadioParam < ParamWithChoices
81
+ def radio?
82
+ true
83
+ end
84
+ end
85
+
86
+ class DropdownParam < ParamWithChoices
87
+ def dropdown?
88
+ true
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,40 @@
1
+ require 'compendium/open_hash'
2
+ require 'compendium/param_types'
3
+ require 'active_support/core_ext/string/inflections'
4
+ require 'active_support/core_ext/object/blank'
5
+
6
+ module Compendium
7
+ class Params < OpenHash
8
+ attr_reader :options
9
+
10
+ def initialize(hash = {}, options = {})
11
+ @options = options
12
+ super(prepare_hash_from_options(hash))
13
+ end
14
+
15
+ protected
16
+
17
+ def prepare_hash_from_options(params)
18
+ params = params.slice(*options.keys)
19
+
20
+ options.each do |option_name, metadata|
21
+ begin
22
+ klass = "Compendium::#{"#{metadata.type}Param".classify}".constantize
23
+ params[option_name] = klass.new(get_default_value(params[option_name], metadata.default), metadata[:choices])
24
+ rescue IndexError
25
+ raise IndexError, "invalid index for #{option_name}"
26
+ end
27
+ end
28
+
29
+ params
30
+ end
31
+
32
+ def get_default_value(current, default)
33
+ if current.blank? and !default.blank?
34
+ default.respond_to?(:call) ? default.call : default
35
+ else
36
+ current
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,94 @@
1
+ require 'compendium/result_set'
2
+ require 'compendium/params'
3
+ require 'collection_of'
4
+
5
+ module Compendium
6
+ class Query
7
+ attr_reader :name, :results, :metrics
8
+ attr_accessor :options, :proc, :through, :report
9
+
10
+ def initialize(name, options, proc)
11
+ @name = name
12
+ @options = options
13
+ @proc = proc
14
+ @metrics = ::Collection[Metric]
15
+ end
16
+
17
+ def initialize_clone(*)
18
+ super
19
+ @metrics = @metrics.clone
20
+ end
21
+
22
+ def run(params, context = self)
23
+ collect_results(params, context)
24
+ collect_metrics(context)
25
+
26
+ @results
27
+ end
28
+
29
+ def add_metric(name, proc, options = {})
30
+ Compendium::Metric.new(name, self.name, proc, options).tap { |m| @metrics << m }
31
+ end
32
+
33
+ def render_table(template, *options, &block)
34
+ Compendium::Presenters::Table.new(template, self, *options, &block).render
35
+ end
36
+
37
+ def render_chart(template, *options, &block)
38
+ Compendium::Presenters::Chart.new(template, self, *options, &block).render
39
+ end
40
+
41
+ def ran?
42
+ !@results.nil?
43
+ end
44
+ alias_method :has_run?, :ran?
45
+
46
+ def nil?
47
+ proc.nil?
48
+ end
49
+
50
+ private
51
+
52
+ def collect_results(params, context)
53
+ args = if through.nil?
54
+ params
55
+ else
56
+ collect_through_query_results(through, params, context)
57
+ end
58
+
59
+ command = context.instance_exec(args, &proc) if proc
60
+ command = fetch_results(command)
61
+ @results = ResultSet.new(command) if command
62
+ end
63
+
64
+ def collect_metrics(context)
65
+ metrics.each{ |m| m.run(context, results) } unless results.empty?
66
+ end
67
+
68
+ def fetch_results(command)
69
+ if options.key?(:through) or options.fetch(:collect, nil) == :active_record
70
+ command
71
+ else
72
+ ::ActiveRecord::Base.connection.select_all(command.respond_to?(:to_sql) ? command.to_sql : command)
73
+ end
74
+ end
75
+
76
+ def collect_through_query_results(through, params, context)
77
+ results = {}
78
+
79
+ through = [through].flatten.map(&method(:get_through_query))
80
+
81
+ through.each do |q|
82
+ q.run(params, context) unless q.ran?
83
+ results[q.name] = q.results.records
84
+ end
85
+
86
+ results = results[through.first.name] if through.size == 1
87
+ results
88
+ end
89
+
90
+ def get_through_query(name)
91
+ report.queries[name]
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,56 @@
1
+ require 'active_support/core_ext/class/attribute_accessors'
2
+ require 'active_support/core_ext/hash/slice'
3
+ require 'compendium/dsl'
4
+
5
+ module Compendium
6
+ class Report
7
+ attr_accessor :params, :results
8
+
9
+ extend Compendium::DSL
10
+
11
+ def self.inherited(report)
12
+ Compendium.reports << report
13
+ end
14
+
15
+ def initialize(params = {})
16
+ @params = Params.new(params, options)
17
+
18
+ # When creating a new report, map each query back to the report
19
+ queries.each { |q| q.report = self }
20
+ end
21
+
22
+ def run(context = nil)
23
+ self.context = context
24
+ self.results = {}
25
+
26
+ queries.each{ |q| self.results[q.name] = q.run(params, ContextWrapper.wrap(context, self)) }
27
+
28
+ self
29
+ end
30
+
31
+ def metrics
32
+ Collection[Metric, queries.map{ |q| q.metrics.to_a }.flatten]
33
+ end
34
+
35
+ private
36
+
37
+ attr_accessor :context
38
+
39
+ def method_missing(name, *args, &block)
40
+ prefix = name.to_s.sub(/(?:_results|\?)\Z/, '').to_sym
41
+
42
+ return queries[name] if queries.keys.include?(name)
43
+ return results[prefix] if name.to_s.end_with? '_results' and queries.keys.include?(prefix)
44
+ return params[name] if options.keys.include?(name)
45
+ return !!params[prefix] if name.to_s.end_with? '?' and options.keys.include?(prefix)
46
+ super
47
+ end
48
+
49
+ def respond_to_missing?(name, include_private = false)
50
+ prefix = name.to_s.sub(/_results\Z/, '').to_sym
51
+ return true if queries.keys.include?(name)
52
+ return true if name.to_s.end_with? '_results' and queries.keys.include?(prefix)
53
+ super
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,24 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module Compendium
4
+ class ResultSet
5
+ delegate :first, :last, :to_a, :empty?, :each, :map, :[], :count, :length, :size, :==, to: :records
6
+
7
+ attr_reader :records
8
+ alias :all :records
9
+
10
+ def initialize(records)
11
+ @records = records.map do |r|
12
+ r.respond_to?(:with_indifferent_access) ? r.with_indifferent_access : r
13
+ end
14
+ end
15
+
16
+ def keys
17
+ records.is_a?(Array) ? first.keys : records.keys
18
+ end
19
+
20
+ def as_json(options = {})
21
+ records.map{ |r| r.except(*options[:except]) }
22
+ end
23
+ end
24
+ end