deli 0.5.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.
data/Rakefile ADDED
@@ -0,0 +1,77 @@
1
+ require 'rake'
2
+ require "rake/rdoctask"
3
+ require 'rake/gempackagetask'
4
+
5
+ spec = Gem::Specification.new do |s|
6
+ s.name = "deli"
7
+ s.authors = ["Lance Pollard"]
8
+ s.version = "0.5.0"
9
+ s.summary = "Tasty URL Parameters for Rails"
10
+ s.homepage = "http://github.com/viatropos/deli"
11
+ s.email = "lancejpollard@gmail.com"
12
+ s.description = "Tasty URL Parameters for Rails"
13
+ s.has_rdoc = false
14
+ s.rubyforge_project = "deli"
15
+ s.platform = Gem::Platform::RUBY
16
+ s.files = %w(Rakefile) + Dir["{lib,rails,spec,app}/**/*"] - Dir["spec/tmp"]
17
+ s.require_path = "lib"
18
+ end
19
+
20
+ Rake::GemPackageTask.new(spec) do |pkg|
21
+ pkg.gem_spec = spec
22
+ end
23
+
24
+ desc 'run unit tests'
25
+ task :test do
26
+ Dir["test/**/*"].each do |file|
27
+ next unless File.basename(file) =~ /test_/
28
+ next unless File.extname(file) == ".rb"
29
+ system "ruby #{file}"
30
+ end
31
+ end
32
+
33
+ desc "Create .gemspec file (useful for github)"
34
+ task :gemspec do
35
+ File.open("#{spec.name}.gemspec", "w") do |f|
36
+ f.puts spec.to_ruby
37
+ end
38
+ end
39
+
40
+ desc "Build the gem into the current directory"
41
+ task :gem => :gemspec do
42
+ `gem build #{spec.name}.gemspec`
43
+ end
44
+
45
+ desc "Publish gem to rubygems"
46
+ task :publish => [:package] do
47
+ %x[gem push #{spec.name}-#{spec.version}.gem]
48
+ end
49
+
50
+ desc "Print a list of the files to be put into the gem"
51
+ task :manifest do
52
+ File.open("Manifest", "w") do |f|
53
+ spec.files.each do |file|
54
+ f.puts file
55
+ end
56
+ end
57
+ end
58
+
59
+ desc "Install the gem locally"
60
+ task :install => [:package] do
61
+ File.mkdir("pkg") unless File.exists?("pkg")
62
+ command = "gem install pkg/#{spec.name}-#{spec.version} --no-ri --no-rdoc"
63
+ command = "sudo #{command}" if ENV["SUDO"] == true
64
+ sh %{#{command}}
65
+ end
66
+
67
+ desc "Generate the rdoc"
68
+ Rake::RDocTask.new do |rdoc|
69
+ files = ["README.markdown", "lib/**/*.rb"]
70
+ rdoc.rdoc_files.add(files)
71
+ rdoc.main = "README.markdown"
72
+ rdoc.title = spec.summary
73
+ end
74
+
75
+ task :yank do
76
+ `gem yank #{spec.name} -v #{spec.version}`
77
+ end
data/lib/deli.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'time'
3
+ require 'date'
4
+ require 'active_support'
5
+ require 'active_support/core_ext'
6
+ require 'uri'
7
+
8
+ $:.unshift File.dirname(__FILE__)
9
+
10
+ require "deli/configuration"
11
+ require "deli/railtie" if defined?(::Rails)
12
+ require "deli/query"
13
+ require "deli/model"
14
+ require "deli/controller"
15
+ require "deli/helper"
16
+ require "deli/pagination"
17
+ require "deli/param"
18
+ require "deli/adapters"
@@ -0,0 +1,9 @@
1
+ module Deli
2
+ module Adapters
3
+ autoload :ActiveRecord, "deli/adapters/active_record"
4
+ autoload :Cassandra, "deli/adapters/cassandra"
5
+ autoload :Mongoid, "deli/adapters/mongoid"
6
+ autoload :Neo4j, "deli/adapters/neo4j"
7
+ autoload :Simple, "deli/adapters/simple"
8
+ end
9
+ end
@@ -0,0 +1,251 @@
1
+ module Deli
2
+ module Adapters
3
+ module ActiveRecord
4
+ class Model < Deli::Model
5
+ def model_type(key, options = {})
6
+ default = options[:type]
7
+
8
+ if default.present?
9
+ value = default.to_sym
10
+ else
11
+ value = model_class.columns.detect { |i| i.name == key }
12
+ value = value.present? ? value.type : nil
13
+ end
14
+
15
+ raise ::ArgumentError.new("Must specify type, such as: `match :#{key}, :type => :string`") if value.blank?
16
+
17
+ case value
18
+ when :datetime, :time
19
+ :time
20
+ when :date
21
+ :date
22
+ when :integer, :float, :decimal, :count
23
+ :number
24
+ when :sort, :order
25
+ :order
26
+ when :string, :text
27
+ :string
28
+ else
29
+ value
30
+ end
31
+ end
32
+ end
33
+
34
+ class Query < ::Deli::Query
35
+ # Converts request parameters into an options Hash for ActiveRecord finder methods.# Outputs a query hash for Mongoid.
36
+ #
37
+ # @example Declare the queries you want to make in your controller.
38
+ #
39
+ # queries :order => "created_at desc" do
40
+ # match :full_name
41
+ # match :created, :field => :created_at, :strict => true
42
+ # match :status, :operators => [:in] # should ultimately have full-on validations
43
+ # match :product, :type => :string, :scope => :with_bookmarked_products
44
+ # end
45
+ #
46
+ # @example Request with all possible parameters
47
+ #
48
+ # "/users?full_name=^lance&created=2011-08-01..t&status=active&product=laptop"
49
+ #
50
+ # @param [Hash] params ({}) The query params hash from the controller.
51
+ #
52
+ # @option params [::Symbol] :type Defaults to the database field type. If you use named scopes
53
+ # to reference assiated models, as of now you must specify the type manually. Haven't looked
54
+ # through the Rails 3 scoping code to figure out how use reflection to get join keys.
55
+ # @option params [::String, ::Symbol] :field If a ::Symbol, the table/collection attribute
56
+ # to query, if a ::String, the fully qualified table/collection + attribute string.
57
+ # @option params [::Array] :operators Optional, refines the allowable query value formats.
58
+ # @option params [::Boolean] :strict (+false) If true, it will raise an error if the
59
+ # URL parameter is not properly formatted (e.g. "2011.." vs. "2011..t")
60
+ # @option params [::Symbol] :scope Optional, a class method on the model for the controller,
61
+ # used as a named scope. In here place your +joins(:x).where(:y)+ code.
62
+ #
63
+ # @return [Hash]
64
+ def render(params)
65
+ hash = super
66
+
67
+ hash["sort"] = default_sort if hash["sort"].blank? && default_sort.present?
68
+
69
+ sort = hash.delete("sort")
70
+ limit = hash.delete("limit") || 20
71
+ page = hash.delete("page") || 1
72
+
73
+ offset = limit.present? && page.present? ? ((page - 1) * limit).to_i : 0
74
+
75
+ keys = []
76
+ values = []
77
+
78
+ hash.values.collect do |array|
79
+ keys << array[0]
80
+ values << array[1..-1]
81
+ end
82
+
83
+ keys.map! {|i| "(#{i})"} if keys.length > 1
84
+ keys = keys.join(" AND ")
85
+ values.flatten!
86
+ conditions = [keys, *values]
87
+
88
+ result = {}
89
+ result[:conditions] = conditions if conditions.present?
90
+ result[:limit] = limit if limit.present? && limit > 0
91
+ result[:offset] = offset if offset.present? && offset > 0
92
+ result[:order] = sort if sort.present?
93
+
94
+ result
95
+ end
96
+ end
97
+
98
+ class Param < ::Deli::Param
99
+ attr_accessor :table_key
100
+
101
+ def initialize(key, options = {})
102
+ super(key, options)
103
+ configure_clauses(options)
104
+ end
105
+
106
+ def parse(value)
107
+ [value]
108
+ end
109
+
110
+ def configure_clauses(options)
111
+ case options[:to]
112
+ when ::String
113
+ self.table_key = options[:to]
114
+ when ::Hash
115
+ join_key = options[:as] || options[:to].keys.first
116
+ join_class = join_key.to_s.singularize.camelize.constantize
117
+ attribute = options[:to].values.first.to_sym
118
+ self.table_key = "#{join_class.table_name}.#{attribute}"
119
+ else
120
+ if model_class.respond_to?(:table_name)
121
+ if association = find_association
122
+ self.table_key = "#{model_class.table_name}.#{key}_id"
123
+ else
124
+ self.table_key = "#{model_class.table_name}.#{key}"
125
+ end
126
+ else
127
+ self.table_key = key
128
+ end
129
+ end
130
+ end
131
+
132
+ # what this responds to
133
+ def operators
134
+ [:equals, :contains, :start, :end]
135
+ end
136
+
137
+ def find_association
138
+ key = self.key.to_sym
139
+ model_class.reflect_on_all_associations.detect do |association|
140
+ association.name == key
141
+ end
142
+ end
143
+
144
+ def render(value)
145
+ keys, values = [], []
146
+
147
+ parse(value).each do |and_collection|
148
+ and_keys = []
149
+
150
+ and_collection.each do |item|
151
+ and_keys << "#{table_key} #{render_operator(item[:operators][0])} ?"
152
+ values << render_value(item[:value], item[:operators][1] || item[:operators][0])
153
+ end
154
+
155
+ keys << and_keys
156
+ end
157
+
158
+ keys.map! { |and_keys| and_keys.join(" AND ") }
159
+
160
+ if keys.length > 1
161
+ [parenthesize!(keys).join(" OR "), *values]
162
+ else
163
+ [keys.join(" OR "), *values]
164
+ end
165
+ end
166
+
167
+ def render_name(*names)
168
+ names.flatten.compact.join(".")
169
+ end
170
+
171
+ def render_value(value, operator = nil)
172
+ value
173
+ end
174
+
175
+ def render_operator(value)
176
+ case value
177
+ when "=~"
178
+ match_operator
179
+ when "!~"
180
+ "NOT #{match_operator}"
181
+ else
182
+ value
183
+ end
184
+ end
185
+
186
+ def match_operator
187
+ postgres? ? "ILIKE" : "LIKE"
188
+ end
189
+
190
+ def parenthesize!(items)
191
+ items.map! { |i| "(#{i})" } if items.length > 1
192
+ items
193
+ end
194
+
195
+ def postgres?
196
+ if @postgres.nil?
197
+ @postgres = defined?(::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) && ::ActiveRecord::Base.connection.is_a?(::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
198
+ end
199
+ @postgres
200
+ end
201
+ end
202
+
203
+ class Time < Param
204
+ include Deli::Param::Time
205
+ end
206
+
207
+ class Date < Time
208
+ include Deli::Param::Date
209
+ end
210
+
211
+ class String < Param
212
+ include Deli::Param::String
213
+
214
+ def render_value(value, operator = nil)
215
+ case operator
216
+ when "=", "!="
217
+ value
218
+ when "^"
219
+ "%#{value}"
220
+ when "$"
221
+ "#{value}%"
222
+ else
223
+ "%#{value}%"
224
+ end
225
+ end
226
+ end
227
+
228
+ class Number < Param
229
+ include Deli::Param::Number
230
+ end
231
+
232
+ class Limit < Param
233
+ include Deli::Param::Limit
234
+ end
235
+
236
+ class Order < Param
237
+ include Deli::Param::Order
238
+
239
+ def render(value)
240
+ parse(value).map do |item|
241
+ "#{render_name(item[:namespace], item[:key])} #{item[:operators][0] == "+" ? "ASC" : "DESC"}"
242
+ end.join(", ")
243
+ end
244
+ end
245
+
246
+ class Offset < Param
247
+ include Deli::Param::Offset
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,52 @@
1
+ module Deli
2
+ module Adapters
3
+ # @todo
4
+ module Cassandra
5
+ class Query < ::Deli::Query
6
+ def render(params)
7
+
8
+ end
9
+ end
10
+
11
+ class Param < ::Deli::Param
12
+ def render(value)
13
+
14
+ end
15
+
16
+ def render_value(value, operator = nil)
17
+ value
18
+ end
19
+ end
20
+
21
+ class Time < Param
22
+ include Deli::Param::Time
23
+ end
24
+
25
+ class Date < Time
26
+ include Deli::Param::Date
27
+ end
28
+
29
+ class String < Param
30
+ include Deli::Param::String
31
+ end
32
+
33
+ class Number < Param
34
+ include Deli::Param::Number
35
+ end
36
+
37
+ class Limit < Param
38
+ include Deli::Param::Limit
39
+ end
40
+
41
+ class Order < Param
42
+ include Deli::Param::Order
43
+
44
+ def render(value)
45
+ parse(value).map do |item|
46
+ [render_name(item[:namespace], item[:key]), item[:operators][0] == "+" ? :asc : :desc]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,180 @@
1
+ module Deli
2
+ module Adapters
3
+ # http://mongoid.org/docs/querying/criteria.html
4
+ module Mongoid
5
+ class Model < Deli::Model
6
+ def model_type(key, options = {})
7
+ default = options[:type]
8
+
9
+ if default.present?
10
+ value = default.to_sym
11
+ else
12
+ value = model_class.fields[key.to_s]
13
+ value = value.present? ? value.type.to_s.underscore.to_sym : nil
14
+ end
15
+
16
+ raise ::ArgumentError.new("Must specify type, such as: `match :#{key}, :type => :string`") if value.blank?
17
+
18
+ case value
19
+ when :datetime, :time
20
+ :time
21
+ when :date
22
+ :date
23
+ when :integer, :float, :decimal, :count
24
+ :number
25
+ when :sort, :order
26
+ :order
27
+ when :string, :text
28
+ :string
29
+ else
30
+ value
31
+ end
32
+ end
33
+ end
34
+
35
+ # Outputs a query hash for Mongoid.
36
+ #
37
+ # @example Declare the queries you want to make in your controller.
38
+ #
39
+ # queries :order => "created_at desc" do
40
+ # match :full_name
41
+ # match :created, :field => :created_at
42
+ # match :status, :type => :array
43
+ # match :product, :type => :string, :field => "products.title"
44
+ # match :favorites, :type => :string, :scope => :favorite_products # does the joins / default conditions work
45
+ # end
46
+ #
47
+ # @example Query for +"title=Hello+World"+
48
+ #
49
+ # {:full_name => {"$in" => "John"}}
50
+ class Query < ::Deli::Query
51
+ def render(params)
52
+ params = super
53
+ end
54
+ end
55
+
56
+ class Param < ::Deli::Param
57
+ def render_value(value, operator = nil)
58
+ value
59
+ end
60
+ end
61
+
62
+ class Time < Param
63
+ include ::Mongoid::Fields::Serializable::Timekeeping
64
+ include Deli::Param::Time
65
+
66
+ def render(value)
67
+ result = {}
68
+
69
+ parse(value).flatten.each do |item|
70
+ case item[:operators][0]
71
+ when ">="
72
+ result["$gte"] = serialize(item[:value]) #time.to_mongo
73
+ when ">"
74
+ result["$gt"] = serialize(item[:value])
75
+ when "<="
76
+ result["$lte"] = serialize(item[:value]) #time.to_mongo
77
+ when "<"
78
+ result["$lt"] = serialize(item[:value])
79
+ when
80
+ result = serialize(item[:value])
81
+ end
82
+ end
83
+
84
+ result
85
+ end
86
+ end
87
+
88
+ class Date < Time
89
+ include Deli::Param::Date
90
+ end
91
+
92
+ class String < Param
93
+ include Deli::Param::String
94
+
95
+ def render(value)
96
+ result = parse(value).map do |children|
97
+ regex = []
98
+ includes = []
99
+ excludes = []
100
+ hash = {}
101
+
102
+ children.each do |item|
103
+ start_operator = item[:operators][1] == "^" ? "^" : ""
104
+ end_operator = item[:operators][1] == "$" ? "$" : ""
105
+
106
+ case item[:operators][0]
107
+ #when "="
108
+ # includes << item[:value]
109
+ #when "!="
110
+ # excludes << item[:value]
111
+ when "!~", "!="
112
+ regex << Regexp.new("(?!#{start_operator}#{item[:value]}#{end_operator})", "i")
113
+ when "=~", "="
114
+ regex << Regexp.new("#{start_operator}#{item[:value]}#{end_operator}", "i")
115
+ end
116
+ end
117
+
118
+ #hash["$in"] = includes if includes.present?
119
+ #hash["$nin"] = excludes if excludes.present?
120
+ hash["$regex"] = Regexp.union(*regex) if regex.present?
121
+ hash
122
+ end
123
+
124
+ result.length > 1 ? {"$or" => result} : result[0]
125
+ end
126
+ end
127
+
128
+ class Number < Param
129
+ include Deli::Param::Number
130
+ end
131
+
132
+ class Limit < Param
133
+ include Deli::Param::Limit
134
+ end
135
+
136
+ class Order < Param
137
+ include Deli::Param::Order
138
+
139
+ def render(value)
140
+ parse(value).map do |item|
141
+ [item[:key].to_sym, item[:operators][0] == "+" ? :asc : :desc]
142
+ end
143
+ end
144
+ end
145
+
146
+ class Offset < Param
147
+ include Deli::Param::Offset
148
+ end
149
+
150
+ # # Match all people with first name Emmanuel using Javascript.
151
+ # { "$where" : "this.first_name == 'Emmanuel'" }
152
+ #
153
+ # # Match all people who live in Berlin, where address is embedded.
154
+ # { "addresses.city" : "Berlin" }
155
+ #
156
+ # # Example queries using symbol h4s to perform more complex criteria.
157
+ # { "title" : { "$all" : [ "Sir" ] } }
158
+ # { "age" : { "$exists" : true } }
159
+ # { "age" : { "$gt" : 18 } }
160
+ # { "age" : { "$gte" : 18 } }
161
+ # { "title" : { "$in" : [ "Sir", "Madam" ] } }
162
+ # { "age" : { "$lt" : 55 } }
163
+ # { "age" : { "$lte" : 55 } }
164
+ # { "title" : { "$ne" : "Mr" } }
165
+ # { "title" : { "$nin" : [ "Esquire" ] } }
166
+ # { "aliases" : { "$size" : 2 } }
167
+ # { "location" : { "$near" : [ 22.50, -21.33 ] } }
168
+ # { "location" : { "$within" : { "$center" => [ [ 50, -40 ], 1 ] } } }
169
+ # 3rd number can be a radius: "22.50,-21.33,7"
170
+ # - if not null, if null
171
+ class Geo < Param
172
+ include Deli::Param::Geo
173
+
174
+ def render(value)
175
+ {"$near" => parse(value).reverse}
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end