irie 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.
@@ -0,0 +1,43 @@
1
+ module Irie
2
+ module Extensions
3
+ # Allows paging of results.
4
+ module Paging
5
+ extend ::ActiveSupport::Concern
6
+ ::Irie.available_extensions[:paging] = '::' + Paging.name
7
+
8
+ included do
9
+ include ::Irie::ParamAliases
10
+
11
+ class_attribute(:number_of_records_in_a_page, instance_writer: true) unless self.respond_to? :number_of_records_in_a_page
12
+
13
+ self.number_of_records_in_a_page = ::Irie.number_of_records_in_a_page
14
+ end
15
+
16
+ def index(options={}, &block)
17
+ logger.debug("Irie::Extensions::Count.index") if ::Irie.debug?
18
+ return super(options, &block) unless aliased_param_present?(:page_count)
19
+ @page_count = (collection.count.to_f / self.number_of_records_in_a_page.to_f).ceil
20
+ return respond_to?(:autorender_page_count, true) ? autorender_page_count(options, &block) : super(options, &block)
21
+ end
22
+
23
+ protected
24
+
25
+ def collection
26
+ logger.debug("Irie::Extensions::Paging.collection") if ::Irie.debug?
27
+ object = super
28
+ page_param_value = aliased_param(:page)
29
+ unless page_param_value.nil?
30
+ page = page_param_value.to_i
31
+ page = 1 if page < 1
32
+ object = object.offset((self.number_of_records_in_a_page * (page - 1)).to_s)
33
+ object = object.limit(self.number_of_records_in_a_page.to_s)
34
+ end
35
+
36
+ logger.debug("Irie::Extensions::Paging.collection: relation.to_sql so far: #{object.to_sql}") if ::Irie.debug? && object.respond_to?(:to_sql)
37
+
38
+ set_collection_ivar object
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ module Irie
2
+ module Extensions
3
+ module Paging
4
+ # Standard rendering of index page count in all formats except html so you don't need views for them.
5
+ module AutorenderPageCount
6
+ extend ::ActiveSupport::Concern
7
+ ::Irie.available_extensions[:autorender_page_count] = '::' + AutorenderPageCount.name
8
+
9
+ protected
10
+
11
+ def autorender_page_count(options={}, &block)
12
+ logger.debug("Irie::Extensions::Paging::AutorenderPageCount.autorender_page_count") if ::Irie.debug?
13
+ render request.format.symbol => { page_count: @page_count }, status: 200, layout: false
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,137 @@
1
+ module Irie
2
+ module Extensions
3
+ # Allows filtering of results using ARel predicates.
4
+ module ParamFilters
5
+ extend ::ActiveSupport::Concern
6
+ ::Irie.available_extensions[:param_filters] = '::' + ParamFilters.name
7
+
8
+ included do
9
+ include ::Irie::ParamAliases
10
+
11
+ class_attribute(:default_filtered_by, instance_writer: true) unless self.respond_to? :default_filtered_by
12
+ class_attribute(:composite_param_to_param_name_and_arel_predicate, instance_writer: true) unless self.respond_to? :composite_param_to_param_name_and_arel_predicate
13
+
14
+ self.default_filtered_by ||= {}
15
+ self.composite_param_to_param_name_and_arel_predicate ||= {}
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ protected
21
+
22
+ # A whitelist of filters and definition of filter options related to request parameters.
23
+ #
24
+ # If no options are provided or the :using option is provided, defines attributes that are queryable through the operation(s) already defined in can_filter_by_default_using, or can specify attributes:
25
+ # can_filter_by :attr_name_1, :attr_name_2 # implied using: [eq] if RestfulJson.can_filter_by_default_using = [:eq]
26
+ # can_filter_by :attr_name_1, :attr_name_2, using: [:eq, :not_eq]
27
+ #
28
+ # When :through is specified, it will take the array supplied to through as 0 to many model names following by an attribute name. It will follow through
29
+ # each association until it gets to the attribute to filter by that via ARel joins, e.g. if the model Foobar has an association to :foo, and on the Foo model there is an assocation
30
+ # to :bar, and you want to filter by bar.name (foobar.foo.bar.name):
31
+ # can_filter_by :my_param_name, through: {foo: {bar: :name}}
32
+ #
33
+ # It also supports param to attribute name/joins via define_params, e.g.
34
+ # define_params car: :car_attr_name
35
+ # can_filter_by :car
36
+ def can_filter_by(*args)
37
+ options = args.extract_options!
38
+
39
+ opt_using = options.delete(:using)
40
+ opt_through = options.delete(:through)
41
+ opt_split = options.delete(:split)
42
+ raise ::Irie::ConfigurationError.new "options #{options.inspect} not supported by can_filter_by" if options.present?
43
+
44
+ self.composite_param_to_param_name_and_arel_predicate = self.composite_param_to_param_name_and_arel_predicate.deep_dup
45
+
46
+ # :using is the default action if no options are present
47
+ if opt_using || options.size == 0
48
+ predicates = Array.wrap(opt_using || self.can_filter_by_default_using)
49
+ predicates.each do |predicate|
50
+ predicate_sym = predicate.to_sym
51
+ args.each do |param_name|
52
+ param_name = param_name.to_s
53
+ if predicate_sym == :eq
54
+ self.composite_param_to_param_name_and_arel_predicate[param_name] = [param_name, :eq, opt_split]
55
+ end
56
+
57
+ self.composite_param_to_param_name_and_arel_predicate["#{param_name}#{self.predicate_prefix}#{predicate}"] = [param_name, predicate_sym, opt_split]
58
+ end
59
+ end
60
+ end
61
+
62
+ if opt_through
63
+ raise ::Irie::ConfigurationError.new "Must use extension :params_to_joins to use can_order_by :through" unless ancestors.include?(::Irie::Extensions::ParamsToJoins)
64
+ args.each do |through_key|
65
+ # note: handles cloning, etc.
66
+ self.define_params(through_key => opt_through)
67
+ end
68
+ end
69
+ end
70
+
71
+ # Specify default filters and predicates to use if no filter is provided by the client with
72
+ # the same param name, e.g. if you have:
73
+ # default_filter_by :attr_name_1, eq: 5
74
+ # default_filter_by :production_date, :creation_date, gt: 1.year.ago, lteq: 1.year.from_now
75
+ # and both attr_name_1 and production_date are supplied by the client, then it would filter
76
+ # by the client's attr_name_1 and production_date and filter creation_date by
77
+ # both > 1 year ago and <= 1 year from now.
78
+ def default_filter_by(*args)
79
+ options = args.extract_options!
80
+
81
+ self.default_filtered_by = self.default_filtered_by.deep_dup
82
+
83
+ args.each do |param_name|
84
+ param_name = param_name.to_s
85
+ if self.default_filtered_by[param_name]
86
+ # have merge create new instance to help avoid subclass inheritance related sharing issues.
87
+ self.default_filtered_by[param_name] = self.default_filtered_by[param_name].merge(options)
88
+ else
89
+ self.default_filtered_by[param_name] = options
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ protected
96
+
97
+ def collection
98
+ logger.debug("Irie::Extensions::ParamFilters.collection") if ::Irie.debug?
99
+ object = super
100
+ already_filtered_by_split_param_names = []
101
+ self.composite_param_to_param_name_and_arel_predicate.each do |composite_param, param_name_and_arel_predicate, split|
102
+ if params.key?(composite_param)
103
+ split_param_name, predicate_sym = *param_name_and_arel_predicate
104
+ converted_param_values = converted_param_values.collect{|p| p.split(*split)}.flatten unless split.blank?
105
+ converted_param_values = convert_param(composite_param, params[composite_param])
106
+ # support for named_params/:through renaming of param name
107
+ attr_sym = attr_sym_for_param(split_param_name)
108
+ join_to_apply = join_for_param(split_param_name)
109
+ object = object.joins(join_to_apply) if join_to_apply
110
+ arel_table_column = get_arel_table(split_param_name)[attr_sym]
111
+ raise ::Irie::ConfigurationError.new "can_filter_by/define_params config problem: could not find arel table/column for param name #{split_param_name.inspect} and/or attr_sym #{attr_sym.inspect}" unless arel_table_column
112
+ object = object.where(arel_table_column.send(predicate_sym, converted_param_values))
113
+ already_filtered_by_split_param_names << split_param_name
114
+ end
115
+ end
116
+
117
+ self.default_filtered_by.each do |split_param_name, predicates_to_default_values|
118
+ unless already_filtered_by_split_param_names.include?(split_param_name) || predicates_to_default_values.blank?
119
+ attr_sym = attr_sym_for_param(split_param_name)
120
+ join_to_apply = join_for_param(split_param_name)
121
+ object = object.joins(join_to_apply) if join_to_apply
122
+ arel_table_column = get_arel_table(split_param_name)[attr_sym]
123
+ raise ::Irie::ConfigurationError.new "default_filter_by/define_params config problem: could not find arel table/column for param name #{split_param_name.inspect} and/or attr_sym #{attr_sym.inspect}" unless arel_table_column
124
+ predicates_to_default_values.each do |predicate_sym, one_or_more_default_values|
125
+ object = object.where(arel_table_column.send(predicate_sym, Array.wrap(one_or_more_default_values)))
126
+ end
127
+ end
128
+ end
129
+
130
+ logger.debug("Irie::Extensions::ParamFilters.collection: relation.to_sql so far: #{object.to_sql}") if ::Irie.debug? && object.respond_to?(:to_sql)
131
+
132
+ set_collection_ivar object
133
+ end
134
+
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,108 @@
1
+ module Irie
2
+ module Extensions
3
+ # Depended on by some extensions to handle joins.
4
+ module ParamsToJoins
5
+ extend ::ActiveSupport::Concern
6
+ ::Irie.available_extensions[:params_to_joins] = '::' + ParamsToJoins.name
7
+
8
+ included do
9
+ class_attribute(:params_to_joins, instance_writer: true) unless self.respond_to? :params_to_joins
10
+
11
+ self.params_to_joins ||= {}
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ protected
17
+
18
+ # An alterative method to defining :through options (for can_filter_by, can_order_by, etc.)
19
+ # in a single place that isn't on the same line as another class method.
20
+ #
21
+ # E.g.:
22
+ #
23
+ # define_params name: {company: {employee: :full_name}},
24
+ # color: :external_color
25
+ # can_filter_by :name
26
+ # default_filter_by :name, eq: 'Guest'
27
+ # can_order_by :color
28
+ # default_filter_by :color, eq: 'blue'
29
+ def define_params(*args)
30
+ options = args.extract_options!
31
+
32
+ raise ::Irie::ConfigurationError.new "define_param(s) only takes a single hash of param name(s) to hash(es)" if args.length > 0
33
+
34
+ self.params_to_joins = self.params_to_joins.deep_dup
35
+
36
+ options.each do |param_name, through_val|
37
+ param_name = param_name.to_s
38
+ self.params_to_joins[param_name.to_s] = (convert = ->(hsh, orig=nil) do
39
+ orig ||= hsh
40
+ case hsh
41
+ when String, Symbol
42
+ {attr_sym: hsh.to_sym}
43
+ when Hash
44
+ case hsh.values.first
45
+ when String, Symbol
46
+ {attr_sym: hsh.values.first.to_sym, joins: hsh.keys.first}
47
+ when Hash
48
+ case hsh.values.first.values.first
49
+ when String, Symbol
50
+ attr_name = hsh.values.first.values.first
51
+ hsh[hsh.keys.first] = hsh.values.first.keys.first
52
+ {attr_sym: attr_name.to_sym, joins: orig}
53
+ when Hash
54
+ convert.call(hsh.values.first, orig)
55
+ else
56
+ raise ::Irie::ConfigurationError.new "Invalid :through option: #{hsh.values.first.values.first} in #{self}"
57
+ end
58
+ else
59
+ raise ::Irie::ConfigurationError.new "Invalid :through option: #{hsh.values.first} in #{self}"
60
+ end
61
+ else
62
+ raise ::Irie::ConfigurationError.new "Invalid :through option: #{hsh} in #{self}"
63
+ end
64
+ end)[through_val.deep_dup]
65
+ end
66
+
67
+ self.params_to_joins
68
+ end
69
+ end
70
+
71
+ protected
72
+
73
+ # Returns attribute name for the param name.
74
+ def attr_sym_for_param(param_name)
75
+ self.params_to_joins[param_name].try(:[], :attr_sym) || param_name
76
+ end
77
+
78
+ # Returns value to be used for the `.joins` query method.
79
+ def join_for_param(param_name)
80
+ self.params_to_joins[param_name].try(:[], :joins)
81
+ end
82
+
83
+ # Walk any configured :through options to get the ARel table or return resource_class.arel_table.
84
+ def get_arel_table(param_name)
85
+ opts = self.params_to_joins[param_name.to_s] || {}
86
+ hsh = opts[:joins]
87
+ return resource_class.arel_table unless hsh && hsh.size > 0
88
+ # find arel_table corresponding to
89
+ find_assoc_resource_class = ->(last_resource_class, assoc_name) do
90
+ next_class = last_resource_class.reflections.map{|refl_assoc_name, refl| refl.class_name.constantize if refl_assoc_name.to_s == assoc_name.to_s}.compact.first
91
+ raise ::Irie::ConfigurationError.new "#{last_resource_class} is missing association #{hsh.values.first} defined in #{self} through option or define_params" unless next_class
92
+ next_class
93
+ end
94
+
95
+ (find_arel_table = ->(last_resource_class, val) do
96
+ case val
97
+ when String, Symbol
98
+ find_assoc_resource_class.call(last_resource_class, val).arel_table
99
+ when Hash
100
+ find_arel_table.call(find_assoc_resource_class.call(last_resource_class, val.keys.first), val.values.first)
101
+ else
102
+ raise ::Irie::ConfigurationError.new "get_arel_table failed because unhandled #{val} in joins in through"
103
+ end
104
+ end)[resource_class, opts[:joins]]
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,58 @@
1
+ module Irie
2
+ module Extensions
3
+ # Allows use of a lambda to work with request parameters to filter results.
4
+ module QueryFilter
5
+ extend ::ActiveSupport::Concern
6
+ ::Irie.available_extensions[:query_filter] = '::' + QueryFilter.name
7
+
8
+ included do
9
+ include ::Irie::ParamAliases
10
+
11
+ class_attribute(:param_to_query, instance_writer: true) unless self.respond_to? :param_to_query
12
+
13
+ self.param_to_query ||= {}
14
+ end
15
+
16
+ module ClassMethods
17
+
18
+ protected
19
+
20
+ # Specify a custom query to filter by if the named request parameter is provided, e.g.
21
+ # can_filter_by_query status: ->(q, status) { status == 'all' ? q : q.where(:status => status) },
22
+ # color: ->(q, color) { color == 'red' ? q.where("color = 'red' or color = 'ruby'") : q.where(:color => color) }
23
+ def can_filter_by_query(*args)
24
+ options = args.extract_options!
25
+
26
+ raise ::Irie::ConfigurationError.new "arguments #{args.inspect} are not supported by can_filter_by_query" if args.length > 0
27
+
28
+ self.param_to_query = self.param_to_query.deep_dup
29
+
30
+ options.each do |param_name, proc|
31
+ self.param_to_query[param_name.to_sym] = proc
32
+ end
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ def collection
39
+ logger.debug("Irie::Extensions::QueryFilter.collection") if ::Irie.debug?
40
+ object = super
41
+ # convert to relation if model class because proc expects a relation
42
+ object = object.all unless object.is_a?(ActiveRecord::Relation)
43
+
44
+ this_includes = self.action_to_query_includes[params[:action].to_sym] || self.all_action_query_includes
45
+ self.param_to_query.each do |param_name, param_query|
46
+ param_value = params[param_name]
47
+ unless param_value.nil?
48
+ object = param_query.call(object, convert_param(param_name.to_s, param_value))
49
+ end
50
+ end
51
+
52
+ logger.debug("Irie::Extensions::QueryFilter.collection: relation.to_sql so far: #{object.to_sql}") if ::Irie.debug? && object.respond_to?(:to_sql)
53
+
54
+ set_collection_ivar object
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,107 @@
1
+ module Irie
2
+ module Extensions
3
+ # Allows ability to do `.includes(...)` on query to avoid n+1 queries.
4
+ module QueryIncludes
5
+ extend ::ActiveSupport::Concern
6
+ ::Irie.available_extensions[:query_includes] = '::' + QueryIncludes.name
7
+
8
+ included do
9
+ include ::Irie::ParamAliases
10
+
11
+ class_attribute(:action_to_query_includes, instance_writer: true) unless self.respond_to? :action_to_query_includes
12
+ class_attribute(:all_action_query_includes, instance_writer: true) unless self.respond_to? :all_action_query_includes
13
+
14
+ self.action_to_query_includes ||= {}
15
+ self.all_action_query_includes ||= []
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ protected
21
+
22
+ # Calls .includes(*args) on all action queries with args provided to query_includes, e.g.:
23
+ # query_includes :category, :comments
24
+ # or:
25
+ # query_includes posts: [{comments: :guest}, :tags]
26
+ # Note that query_includes_for overrides includes specified by query_includes.
27
+ def query_includes(*args)
28
+ options = args.extract_options!
29
+
30
+ self.all_action_query_includes = self.all_action_query_includes.deep_dup
31
+ old_options = self.all_action_query_includes.extract_options!
32
+ self.all_action_query_includes = (self.all_action_query_includes + args).uniq
33
+ self.all_action_query_includes << old_options.merge(options)
34
+ end
35
+
36
+ # Calls .includes(...) only on specified action queries, e.g.:
37
+ # query_includes_for :create, :update, are: [:category, :comments]
38
+ # query_includes_for :index, are: [posts: [{comments: :guest}, :tags]]
39
+ def query_includes_for(*args)
40
+ options = args.extract_options!
41
+
42
+ opt_are = options.delete(:are)
43
+ raise ::Irie::ConfigurationError.new "options #{options.inspect} not supported by can_filter_by" if options.present?
44
+
45
+ self.action_to_query_includes = self.action_to_query_includes.deep_dup
46
+
47
+ args.each do |an_action|
48
+ if opt_are
49
+ (self.action_to_query_includes ||= {}).merge!({an_action.to_sym => opt_are})
50
+ else
51
+ raise ::Irie::ConfigurationError.new "#{self.class.name} must supply an :are option with includes_for #{an_action.inspect}"
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ protected
58
+
59
+ def collection
60
+ logger.debug("Irie::Extensions::QueryIncludes.collection") if ::Irie.debug?
61
+ object = super
62
+
63
+ this_includes = self.action_to_query_includes[params[:action].to_sym] || self.all_action_query_includes
64
+ if this_includes && this_includes.size > 0
65
+ object = object.includes(*this_includes)
66
+ else
67
+ object
68
+ end
69
+
70
+ logger.debug("Irie::Extensions::QueryIncludes.collection: relation.to_sql so far: #{object.to_sql}") if ::Irie.debug? && object.respond_to?(:to_sql)
71
+
72
+ set_collection_ivar object
73
+ end
74
+
75
+ def resource
76
+ logger.debug("Irie::Extensions::QueryIncludes.resource") if ::Irie.debug?
77
+ this_includes = self.action_to_query_includes[params[:action].to_sym] || self.all_action_query_includes
78
+ if this_includes && this_includes.size > 0
79
+ # can return the model class, so won't call bang (includes!) method
80
+ object = end_of_association_chain.includes(*this_includes)
81
+
82
+ logger.debug("Irie::Extensions::QueryIncludes.resource: end_of_association_chain.to_sql: #{object.to_sql}") if ::Irie.debug? && object.respond_to?(:to_sql)
83
+
84
+ set_resource_ivar object.send(method_for_find, params[:id])
85
+ else
86
+ super
87
+ end
88
+ end
89
+
90
+ def build_resource
91
+ logger.debug("Irie::Extensions::QueryIncludes.build_resource") if ::Irie.debug?
92
+ this_includes = self.action_to_query_includes[params[:action].to_sym] || self.all_action_query_includes
93
+ if this_includes && this_includes.size > 0
94
+ # can return the model class, so won't call bang (includes!) method
95
+ object = end_of_association_chain.includes(*this_includes)
96
+
97
+ logger.debug("Irie::Extensions::QueryIncludes.build_resource: end_of_association_chain.to_sql: #{object.to_sql}") if ::Irie.debug? && object.respond_to?(:to_sql)
98
+
99
+ set_resource_ivar object.send(method_for_build, *resource_params)
100
+ else
101
+ object
102
+ end
103
+ end
104
+
105
+ end
106
+ end
107
+ end