deli 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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