httpsql 0.1.2 → 0.2.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/.coveralls.yml +1 -0
- data/.gitignore +3 -0
- data/README.md +58 -16
- data/gemfiles/activerecord_3.1.gemfile +1 -1
- data/gemfiles/activerecord_3.2.gemfile +1 -1
- data/gemfiles/activerecord_4.0.gemfile +2 -1
- data/httpsql.gemspec +4 -2
- data/lib/httpsql.rb +84 -21
- data/lib/httpsql/version.rb +1 -1
- data/test/httpsql_test.rb +237 -88
- data/test/test_helper.rb +78 -5
- metadata +41 -8
data/.coveralls.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
repo_token: mUabjcN9fjNpphNtEUHYDJtE1VzEa8p12
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
# Httpsql
|
|
2
2
|
|
|
3
3
|
[](http://badge.fury.io/rb/httpsql)
|
|
4
|
-
[](https://travis-ci.org/Adaptly/httpsql)
|
|
4
|
+
[](https://travis-ci.org/Adaptly/httpsql)
|
|
5
5
|
[](https://codeclimate.com/github/Adaptly/httpsql)
|
|
6
6
|
[](https://gemnasium.com/Adaptly/httpsql)
|
|
7
7
|
[](https://coveralls.io/r/Adaptly/httpsql)
|
|
8
8
|
|
|
9
|
-
Httpsql is a module, designed to be included in [Active
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
using the
|
|
9
|
+
Httpsql is a module, designed to be included in [Active
|
|
10
|
+
Record](http://api.rubyonrails.org/classes/ActiveRecord/Base.html) models
|
|
11
|
+
exposed by [grape](https://github.com/intridea/grape). Once the module is
|
|
12
|
+
included, a given model can respond directly to query params passed to it,
|
|
13
|
+
using `with_params`. You can constrain the fields returned by the model, using
|
|
14
|
+
the `field` query parameter. You can also use httpsql to group, join, and order
|
|
15
|
+
results. Currently, only inner joins are supported, using rudimentary Active
|
|
16
|
+
Record joins.
|
|
14
17
|
|
|
15
18
|
Httpsql uses [ARel](http://www.slideshare.net/flah00/activerecord-arel) to
|
|
16
19
|
generate queries and exposes ARel's methods via query params. The supported ARel
|
|
17
|
-
methods are eq, not_eq, matches, does_not_match, gt, gteq, lt, lteq
|
|
20
|
+
methods are eq, not_eq, matches, does_not_match, gt, gteq, lt, lteq, sum,
|
|
21
|
+
maximum, and minimum. It is also possible to pass columns to arbitrary SQL
|
|
22
|
+
functions.
|
|
18
23
|
|
|
19
24
|
Httpsql also generates documentaion for endpoints, which can be easily merged
|
|
20
|
-
into your existing documentation (`#
|
|
25
|
+
into your existing documentation (`#grape_documentation`).
|
|
21
26
|
|
|
22
|
-
Httpsql reserves
|
|
23
|
-
|
|
27
|
+
Httpsql reserves the following parameters access_token, field, group, join, and order. If
|
|
28
|
+
your model has any columns by the same name, you'll need to rename them.
|
|
24
29
|
|
|
25
30
|
## Installation
|
|
26
31
|
|
|
@@ -48,13 +53,29 @@ Assume you have a model, widget, whose fields are id, int_field, string_field, c
|
|
|
48
53
|
t.datetime "updated_at", :null => false
|
|
49
54
|
end
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
create_table "dongles", :force => true do |t|
|
|
57
|
+
t.integer "widget_id"
|
|
58
|
+
t.string "description"
|
|
59
|
+
t.datetime "created_at", :null => false
|
|
60
|
+
t.datetime "updated_at", :null => false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
### widget.rb
|
|
52
64
|
|
|
53
65
|
class Widget < ActiveRecord::Base
|
|
54
66
|
include Httpsql
|
|
67
|
+
has_many :dongles
|
|
55
68
|
attr_accessible :int_field, :dec_field, :string_field
|
|
56
69
|
end
|
|
57
70
|
|
|
71
|
+
### dongle.rb
|
|
72
|
+
|
|
73
|
+
class Dongle < ActiveRecord::Base
|
|
74
|
+
include Httpsql
|
|
75
|
+
belongs_to :widget
|
|
76
|
+
attr_accessible :description
|
|
77
|
+
end
|
|
78
|
+
|
|
58
79
|
### api.rb
|
|
59
80
|
|
|
60
81
|
class Api < Grape::API
|
|
@@ -63,13 +84,25 @@ Assume you have a model, widget, whose fields are id, int_field, string_field, c
|
|
|
63
84
|
default_format :json
|
|
64
85
|
|
|
65
86
|
resource :widgets do
|
|
66
|
-
desc 'Get all widgets'
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
desc 'Get all widgets'
|
|
88
|
+
params do
|
|
89
|
+
Widget.grape_documentation(self)
|
|
90
|
+
end
|
|
69
91
|
get '/' do
|
|
70
|
-
|
|
92
|
+
Widget.with_params(params)
|
|
71
93
|
end
|
|
72
94
|
end
|
|
95
|
+
|
|
96
|
+
resource :dongles do
|
|
97
|
+
desc 'Get all dongles'
|
|
98
|
+
params do
|
|
99
|
+
Dongle.grape_documentation(self)
|
|
100
|
+
end
|
|
101
|
+
get '/' do
|
|
102
|
+
Dongle.with_params(params)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
73
106
|
end
|
|
74
107
|
|
|
75
108
|
### config.ru
|
|
@@ -102,9 +135,18 @@ Query your new API
|
|
|
102
135
|
curl 'http://localhost:3000/api/v1/widgets?id[]=1&id[]=2&created_at.gt=2013-06-01'
|
|
103
136
|
SELECT * FROM widgets WHERE id IN (1,2) AND created_at > '2013-06-01'
|
|
104
137
|
|
|
105
|
-
curl 'http://localhost:3000/api/v1/widgets?id[]=1&id[]=2&created_at.gt=2013-06-01&
|
|
138
|
+
curl 'http://localhost:3000/api/v1/widgets?id[]=1&id[]=2&created_at.gt=2013-06-01&field[]=id&field[]=int_field'
|
|
106
139
|
SELECT id, int_field FROM widgets WHERE id IN (1,2) AND created_at > '2013-06-01'
|
|
107
140
|
|
|
141
|
+
curl 'http://localhost:3000/api/v1/widgets?order=widgets.int_field+desc'
|
|
142
|
+
SELECT * FROM widgets ORDER BY widgets.int_field desc
|
|
143
|
+
|
|
144
|
+
curl 'http://localhost:3000/api/v1/widgets?group=widgets.int_field'
|
|
145
|
+
SELECT * FROM widgets GROUP BY widgets.int_field
|
|
146
|
+
|
|
147
|
+
curl 'http://localhost:3000/api/v1/dongles?field[]=description&field[]=widgets.int_field&join[]=widgets&group[]=widgets.string_field&order=string_field'
|
|
148
|
+
SELECT dongles.description, widgets.int_field FROM dongles INNER JOIN widgets ON (widgets.id = dongles.widget_id) GROUP BY widgets.string_field ORDER BY dongles.string_field
|
|
149
|
+
|
|
108
150
|
curl 'http://localhost:3000/api/v1/widgets?int_field.sum'
|
|
109
151
|
SELECT SUM(int_field) AS int_field FROM widgets
|
|
110
152
|
|
data/httpsql.gemspec
CHANGED
|
@@ -18,11 +18,13 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
19
|
spec.require_paths = ["lib"]
|
|
20
20
|
|
|
21
|
-
spec.add_dependency "activerecord", "
|
|
21
|
+
spec.add_dependency "activerecord", "~> 3.2"
|
|
22
22
|
spec.add_dependency "arel", ">= 2.2"
|
|
23
|
-
spec.add_dependency "grape"
|
|
23
|
+
spec.add_dependency "grape", ">= 0.5.0"
|
|
24
24
|
spec.add_development_dependency "bundler", "~> 1.3"
|
|
25
25
|
spec.add_development_dependency "coveralls", ">= 0.5.7"
|
|
26
|
+
spec.add_development_dependency "minitest", "~> 4.2"
|
|
27
|
+
spec.add_development_dependency "pry-nav"
|
|
26
28
|
spec.add_development_dependency "rake"
|
|
27
29
|
spec.add_development_dependency "simplecov", ">= 0.7"
|
|
28
30
|
spec.add_development_dependency "sqlite3"
|
data/lib/httpsql.rb
CHANGED
|
@@ -6,14 +6,35 @@ module Httpsql
|
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
module ClassMethods
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
# The method to call within API end points
|
|
10
|
+
# @param params [Hash] The params hash for a given API request
|
|
11
|
+
# @return [ActiveRecord::Relation]
|
|
12
|
+
# @example Constraining a model index end point
|
|
13
|
+
# resource :my_models
|
|
14
|
+
# get '/' do
|
|
15
|
+
# MyModel.with_params(params)
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
def with_params(params={})
|
|
19
|
+
fields = []
|
|
20
|
+
conds = []
|
|
21
|
+
joins = []
|
|
22
|
+
groups = []
|
|
23
|
+
orders = []
|
|
12
24
|
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
fields += Array(params[:field] || params['field'])
|
|
26
|
+
|
|
27
|
+
httpsql_valid_params(params).each do |k,v|
|
|
28
|
+
k, m = k.to_s.split('.')
|
|
15
29
|
next if k.to_s == 'access_token'
|
|
16
|
-
|
|
30
|
+
|
|
31
|
+
if k == 'join'
|
|
32
|
+
joins += Array(v).map!(&:to_sym)
|
|
33
|
+
elsif k == 'order'
|
|
34
|
+
orders += Array(v).map!{|w| httpsql_quote_value_with_args(w)}
|
|
35
|
+
elsif k == 'group'
|
|
36
|
+
groups += Array(v).map!{|w| httpsql_quote_value(w)}
|
|
37
|
+
elsif m
|
|
17
38
|
if %w(sum minimum maximum).include?(m)
|
|
18
39
|
fields << arel_table[k].send(m).as(k)
|
|
19
40
|
elsif !arel_table[k].respond_to?(m)
|
|
@@ -32,29 +53,71 @@ module Httpsql
|
|
|
32
53
|
conds = conds.compact.inject{|x,y| x.and(y)}
|
|
33
54
|
|
|
34
55
|
ar_rel = where(conds)
|
|
56
|
+
ar_rel = ar_rel.joins(joins) if joins.any?
|
|
57
|
+
ar_rel = ar_rel.group(groups) if groups.any?
|
|
58
|
+
ar_rel = ar_rel.order(orders) if orders.any?
|
|
35
59
|
ar_rel = ar_rel.select(fields) if fields.any?
|
|
36
60
|
ar_rel
|
|
37
61
|
end
|
|
38
62
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
# Provide documentation for Grape end points
|
|
64
|
+
# @param ctx [Object] The calling object
|
|
65
|
+
# @return [Hash]
|
|
66
|
+
# @example Including documentation
|
|
67
|
+
# params do
|
|
68
|
+
# MyModel.grape_documentation(self)
|
|
69
|
+
# end
|
|
70
|
+
def grape_documentation(ctx=nil)
|
|
71
|
+
columns.each do |c|
|
|
72
|
+
opt_hash = {}
|
|
73
|
+
if (k = httpsql_sql_type_conversion(c.type))
|
|
74
|
+
opt_hash[:type] = k
|
|
75
|
+
end
|
|
76
|
+
ctx.optional c.name, opt_hash
|
|
77
|
+
end
|
|
78
|
+
ctx.optional :field, type: Array, desc: "An array of strings: fields to select from the database"
|
|
79
|
+
ctx.optional :group, type: Array, desc: "An array of strings: fields to group by"
|
|
80
|
+
ctx.optional :order, type: Array, desc: "An array of strings: fields to order by"
|
|
81
|
+
ctx.optional :join, type: Array, desc: "An array of strings: tables to join (#{httpsql_join_tables.join(',')})"
|
|
52
82
|
end
|
|
53
83
|
|
|
54
84
|
private
|
|
55
|
-
def
|
|
56
|
-
params.select{|k,v| column_names.include?(k.to_s.split('.').first)}
|
|
85
|
+
def httpsql_valid_params(params)
|
|
86
|
+
params.select{|k,v| [*column_names, 'join', 'group', 'order'].include?(k.to_s.split('.').first)}
|
|
57
87
|
end
|
|
88
|
+
|
|
89
|
+
def httpsql_quote_value(v)
|
|
90
|
+
v['.'].nil? ? arel_table[v] : v.split('.').map!{|x| connection.quote_table_name(x)}.join('.')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def httpsql_quote_value_with_args(v)
|
|
94
|
+
match = v.match(/([^\s]+)(?:\s+(.*))?/)
|
|
95
|
+
q = httpsql_quote_value(match[1])
|
|
96
|
+
if match[2]
|
|
97
|
+
if q.kind_of?(String)
|
|
98
|
+
q + " " + match[2]
|
|
99
|
+
else
|
|
100
|
+
q.send(match[2])
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def httpsql_sql_type_conversion(type)
|
|
106
|
+
case type.to_sym
|
|
107
|
+
when :bigint then Bignum
|
|
108
|
+
when :date then Date
|
|
109
|
+
when :datetime then Time
|
|
110
|
+
when :float, :decimal then Float
|
|
111
|
+
when :integer then Fixnum
|
|
112
|
+
when :string, /text/, /^char/, /^varchar/ then String
|
|
113
|
+
else nil
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def httpsql_join_tables
|
|
118
|
+
@join_tables ||= reflections.keys
|
|
119
|
+
end
|
|
120
|
+
|
|
58
121
|
end
|
|
59
122
|
end
|
|
60
123
|
|
data/lib/httpsql/version.rb
CHANGED
data/test/httpsql_test.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require 'test_helper'
|
|
2
2
|
|
|
3
|
-
def
|
|
3
|
+
def generate_foo_models
|
|
4
4
|
FooModel.create!([
|
|
5
5
|
{int_field: 0, dec_field: 0.01, string_field: "zero", access_token: "000"},
|
|
6
6
|
{int_field: 1, dec_field: 1.01, string_field: "one", access_token: "111"},
|
|
@@ -10,120 +10,269 @@ def generate_models
|
|
|
10
10
|
])
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def generate_bar_models
|
|
14
|
+
BarModel.create!([
|
|
15
|
+
{foo_model_id: 1, string_field: "zero"},
|
|
16
|
+
{foo_model_id: 2, string_field: "one"},
|
|
17
|
+
])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def generate_baz_models
|
|
21
|
+
BazModel.create!([
|
|
22
|
+
{foo_model_id: 1, string_field: "zeropointzero"},
|
|
23
|
+
{foo_model_id: 1, string_field: "zeropointone"},
|
|
24
|
+
{foo_model_id: 2, string_field: "onepointzero"},
|
|
25
|
+
{foo_model_id: 2, string_field: "onepointone"},
|
|
26
|
+
])
|
|
27
|
+
end
|
|
28
|
+
|
|
13
29
|
describe Httpsql do
|
|
14
30
|
before :each do
|
|
15
31
|
FooModel.connection.execute %Q{DELETE FROM foo_models}
|
|
32
|
+
FooModel.connection.execute %Q{DELETE FROM bar_models}
|
|
33
|
+
FooModel.connection.execute %Q{DELETE FROM baz_models}
|
|
16
34
|
end
|
|
17
35
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
36
|
+
describe "#httpsql_valid_params" do
|
|
37
|
+
it 'selects a model\'s columns from a given hash' do
|
|
38
|
+
ret = FooModel.send(:httpsql_valid_params, id: 1, int_field: 2, string_field: "foo", access_token: "a", created_at: '2013-01-01T00:00:00', created_at: '2013-01-01T00:00:00', foo: :bar)
|
|
39
|
+
ret.must_equal(id: 1, int_field: 2, string_field: "foo", access_token: "a", created_at: '2013-01-01T00:00:00', created_at: '2013-01-01T00:00:00')
|
|
40
|
+
end
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
it 'selects reserved parameters' do
|
|
43
|
+
ret = FooModel.send(:httpsql_valid_params, 'join'=>'foo', 'group'=>'bar', 'order'=>'baz')
|
|
44
|
+
ret.must_equal('join'=>'foo', 'group'=>'bar', 'order'=>'baz')
|
|
45
|
+
end
|
|
26
46
|
end
|
|
27
47
|
|
|
28
|
-
|
|
29
|
-
models
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
describe '#with_params' do
|
|
49
|
+
it 'selects all models' do
|
|
50
|
+
models = generate_foo_models
|
|
51
|
+
FooModel.with_params({}).must_equal models
|
|
52
|
+
end
|
|
32
53
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
54
|
+
it 'selects a specified array of models' do
|
|
55
|
+
models = generate_foo_models
|
|
56
|
+
FooModel.with_params("int_field" => [0, 1]).must_equal models[0..1]
|
|
57
|
+
end
|
|
37
58
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
it 'selects a model, using eq' do
|
|
60
|
+
models = generate_foo_models
|
|
61
|
+
FooModel.with_params("int_field.eq" => 0).must_equal [models[0]]
|
|
62
|
+
end
|
|
42
63
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
64
|
+
it 'selects models, using not_eq' do
|
|
65
|
+
models = generate_foo_models
|
|
66
|
+
FooModel.with_params("int_field.not_eq" => 0).must_equal models[1..-1]
|
|
67
|
+
end
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
it 'selects models, using gt' do
|
|
53
|
-
models = generate_models
|
|
54
|
-
FooModel.where_params_eq("int_field.gt" => 1).must_equal models[2..-1]
|
|
55
|
-
end
|
|
69
|
+
it 'selects a model, using matches' do
|
|
70
|
+
models = generate_foo_models
|
|
71
|
+
FooModel.with_params("string_field.matches" => "%hre%").must_equal [models[3]]
|
|
72
|
+
end
|
|
56
73
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
it 'selects models, using does_not_match' do
|
|
75
|
+
models = generate_foo_models
|
|
76
|
+
FooModel.with_params("string_field.does_not_match" => "%ero").must_equal models[1..-1]
|
|
77
|
+
end
|
|
78
|
+
it 'selects models, using gt' do
|
|
79
|
+
models = generate_foo_models
|
|
80
|
+
FooModel.with_params("int_field.gt" => 1).must_equal models[2..-1]
|
|
81
|
+
end
|
|
61
82
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
it 'selects models, using gteq' do
|
|
84
|
+
models = generate_foo_models
|
|
85
|
+
FooModel.with_params("int_field.gteq" => 2).must_equal models[2..-1]
|
|
86
|
+
end
|
|
66
87
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
it 'select models, using lt' do
|
|
89
|
+
models = generate_foo_models
|
|
90
|
+
FooModel.with_params("int_field.lt" => 1).must_equal [models[0]]
|
|
91
|
+
end
|
|
71
92
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
it 'selects models, using lteq' do
|
|
94
|
+
models = generate_foo_models
|
|
95
|
+
FooModel.with_params("int_field.lteq" => 2).must_equal models[0..2]
|
|
96
|
+
end
|
|
76
97
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
it 'selects models, using two ARel methods' do
|
|
99
|
+
models = generate_foo_models
|
|
100
|
+
FooModel.with_params("int_field.gteq" => 1, "id.gt" => 4).must_equal [models[4]]
|
|
101
|
+
end
|
|
81
102
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
it 'ignores access_token' do
|
|
104
|
+
models = generate_foo_models
|
|
105
|
+
FooModel.with_params("access_token" => "111").must_equal models
|
|
106
|
+
end
|
|
86
107
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
it 'ignores access_token dot notation' do
|
|
109
|
+
models = generate_foo_models
|
|
110
|
+
FooModel.with_params("access_token.eq" => "111").must_equal models
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'selects a model with specified fields' do
|
|
114
|
+
generate_foo_models
|
|
115
|
+
model = FooModel.select([:int_field, :id]).where(int_field: 0)
|
|
116
|
+
FooModel.with_params("int_field.eq" => 0, field: [:int_field, :id]).must_equal model
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'sums the specified field' do
|
|
120
|
+
models = generate_foo_models
|
|
121
|
+
expected = models.collect(&:int_field).inject(:+)
|
|
122
|
+
FooModel.with_params("int_field.sum" => nil).first.int_field.must_equal expected
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'selects the maximum value for the specified field' do
|
|
126
|
+
models = generate_foo_models
|
|
127
|
+
expected = models.collect(&:int_field).max
|
|
128
|
+
FooModel.with_params("int_field.maximum" => nil).first.int_field.must_equal expected
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'selects the minimum value for the specified field' do
|
|
132
|
+
models = generate_foo_models
|
|
133
|
+
expected = models.collect(&:int_field).min
|
|
134
|
+
FooModel.with_params("int_field.minimum" => nil).first.int_field.must_equal expected
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'generates the specified sql for rounding' do
|
|
138
|
+
models = generate_foo_models
|
|
139
|
+
expected = models.collect(&:dec_field).map{|v| v.round.to_f}
|
|
140
|
+
FooModel.with_params("dec_field.round" => "1").collect(&:dec_field).map{|v| v.to_f}.must_equal expected
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'groups correctly' do
|
|
144
|
+
models = generate_foo_models
|
|
145
|
+
expected = [FooModel.create({created_at: "1900-01-01"}), models.last]
|
|
146
|
+
FooModel.with_params("group" => "created_at").must_equal expected
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'orders unqualified fields correctly' do
|
|
150
|
+
models = generate_foo_models
|
|
151
|
+
expected = models.reverse
|
|
152
|
+
FooModel.with_params("order" => "int_field desc").must_equal expected
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it 'orders qualified fields correctly' do
|
|
156
|
+
models = generate_foo_models
|
|
157
|
+
expected = models.reverse
|
|
158
|
+
FooModel.with_params("order" => "foo_models.int_field desc").must_equal expected
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'joins has_one relations' do
|
|
162
|
+
models = generate_foo_models
|
|
163
|
+
generate_bar_models
|
|
164
|
+
FooModel.with_params("join" => "bar_model").to_a.must_equal models[0..1]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'joins has_many relations' do
|
|
168
|
+
models = generate_foo_models
|
|
169
|
+
generate_baz_models
|
|
170
|
+
FooModel.with_params("join" => "baz_models").to_a.must_equal [models[0], models[0], models[1], models[1]]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'joins has_many relations and uses field and group' do
|
|
174
|
+
models = generate_foo_models
|
|
175
|
+
generate_baz_models
|
|
176
|
+
expected = [
|
|
177
|
+
{"int_field"=>1, "string_field"=>"onepointone"},
|
|
178
|
+
{"int_field"=>1, "string_field"=>"onepointzero"},
|
|
179
|
+
{"int_field"=>0, "string_field"=>"zeropointone"},
|
|
180
|
+
{"int_field"=>0, "string_field"=>"zeropointzero"},
|
|
181
|
+
]
|
|
182
|
+
## TODO: sqlite3 && activerecord-4.0 insert an id field... why!?
|
|
183
|
+
expected.map! do |e|
|
|
184
|
+
e["id"] = nil
|
|
185
|
+
e
|
|
186
|
+
end if ActiveRecord::VERSION::MAJOR >= 4
|
|
187
|
+
query = BazModel.with_params("join" => "foo_model",
|
|
188
|
+
"field" => ["foo_models.int_field", "baz_models.string_field"],
|
|
189
|
+
"group" => ["baz_models.string_field"],
|
|
190
|
+
"order" => ["baz_models.string_field"])
|
|
191
|
+
query.collect(&:attributes).must_equal expected
|
|
192
|
+
end
|
|
92
193
|
|
|
93
|
-
it 'sums the specified field' do
|
|
94
|
-
models = generate_models
|
|
95
|
-
expected = models.collect(&:int_field).inject(:+)
|
|
96
|
-
FooModel.where_params_eq("int_field.sum" => nil).first.int_field.must_equal expected
|
|
97
194
|
end
|
|
98
195
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
196
|
+
describe "#httpsql_quote_value" do
|
|
197
|
+
it "quotes absolute names" do
|
|
198
|
+
FooModel.send(:httpsql_quote_value, "foo.bar").must_equal "\"foo\".\"bar\""
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "converts column names to arel nodes" do
|
|
202
|
+
FooModel.send(:httpsql_quote_value, "int_field").must_be_kind_of Arel::Attributes::Attribute
|
|
203
|
+
end
|
|
103
204
|
end
|
|
104
205
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
206
|
+
describe "#httpsql_quote_value_with_args" do
|
|
207
|
+
it "quotes absolute names" do
|
|
208
|
+
FooModel.send(:httpsql_quote_value, "foo.bar").must_equal "\"foo\".\"bar\""
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "quotes absolute names with args" do
|
|
212
|
+
FooModel.send(:httpsql_quote_value_with_args, "foo.bar baz").must_equal "\"foo\".\"bar\" baz"
|
|
213
|
+
end
|
|
109
214
|
end
|
|
110
215
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
216
|
+
describe '#httpsql_sql_type_conversion' do
|
|
217
|
+
it 'identifies bigint as Bignum' do
|
|
218
|
+
FooModel.send(:httpsql_sql_type_conversion, 'bigint').must_equal Bignum
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it 'identifies date as Date' do
|
|
222
|
+
FooModel.send(:httpsql_sql_type_conversion, 'date').must_equal Date
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it 'identifies datetime as Time' do
|
|
226
|
+
FooModel.send(:httpsql_sql_type_conversion, 'datetime').must_equal Time
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it 'identifies float as Float' do
|
|
230
|
+
FooModel.send(:httpsql_sql_type_conversion, 'float').must_equal Float
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'identifies decimal as Bignum' do
|
|
234
|
+
FooModel.send(:httpsql_sql_type_conversion, 'decimal').must_equal Float
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it 'identifies integer as Fixnum' do
|
|
238
|
+
FooModel.send(:httpsql_sql_type_conversion, 'integer').must_equal Fixnum
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it 'identifies string as String' do
|
|
242
|
+
FooModel.send(:httpsql_sql_type_conversion, 'string').must_equal String
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it 'identifies text as String' do
|
|
246
|
+
FooModel.send(:httpsql_sql_type_conversion, 'text').must_equal String
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it 'identifies character varying(255) as String' do
|
|
250
|
+
FooModel.send(:httpsql_sql_type_conversion, 'character varying(255)').must_equal String
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it 'identifies something as nil' do
|
|
254
|
+
FooModel.send(:httpsql_sql_type_conversion, 'something').must_be_nil
|
|
255
|
+
end
|
|
115
256
|
end
|
|
116
257
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
258
|
+
describe '#grape_documentation' do
|
|
259
|
+
it 'generates the correct documentation for version 0.5.x' do
|
|
260
|
+
TestApi.routes.first.route_params.must_equal({
|
|
261
|
+
"id" => {:required => false, :type => "Fixnum"},
|
|
262
|
+
"int_field" => {:required => false, :type => "Fixnum"},
|
|
263
|
+
"dec_field" => {:required => false, :type => "Float"},
|
|
264
|
+
"string_field" => {:required => false, :type => "String"},
|
|
265
|
+
"access_token" => {:required => false, :type => "String"},
|
|
266
|
+
"created_at" => {:required => false, :type => "String"},
|
|
267
|
+
"updated_at" => {:required => false, :type => "String"},
|
|
268
|
+
"field" => {:required => false, :type => "Array", :desc => "An array of strings: fields to select from the database"},
|
|
269
|
+
"group" => {:required => false, :type => "Array", :desc => "An array of strings: fields to group by"},
|
|
270
|
+
"order" => {:required => false, :type => "Array", :desc => "An array of strings: fields to order by"},
|
|
271
|
+
"join" => {:required => false, :type => "Array", :desc => "An array of strings: tables to join (bar_model,baz_models)"}
|
|
272
|
+
|
|
273
|
+
})
|
|
274
|
+
end
|
|
275
|
+
|
|
128
276
|
end
|
|
277
|
+
|
|
129
278
|
end
|
data/test/test_helper.rb
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
require 'simplecov'
|
|
2
|
+
#SimpleCov.start
|
|
3
|
+
require 'coveralls'
|
|
4
|
+
Coveralls.wear!
|
|
5
|
+
|
|
1
6
|
require 'minitest/spec'
|
|
2
7
|
require 'minitest/autorun'
|
|
3
8
|
require 'active_record'
|
|
9
|
+
require 'grape'
|
|
4
10
|
require 'httpsql'
|
|
5
11
|
|
|
6
|
-
require 'simplecov'
|
|
7
|
-
require 'coveralls'
|
|
8
|
-
Coveralls.wear!
|
|
9
|
-
|
|
10
12
|
ActiveRecord::Base.configurations[:test] = {adapter: 'sqlite3', database: 'tmp/httpsql_test'}
|
|
11
13
|
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[:test])
|
|
12
14
|
ActiveRecord::Base.connection.execute %Q{ DROP TABLE IF EXISTS foo_models }
|
|
@@ -22,8 +24,79 @@ ActiveRecord::Base.connection.execute %Q{
|
|
|
22
24
|
primary key(id)
|
|
23
25
|
);
|
|
24
26
|
}
|
|
27
|
+
|
|
28
|
+
ActiveRecord::Base.connection.execute %Q{ DROP TABLE IF EXISTS bar_models }
|
|
29
|
+
ActiveRecord::Base.connection.execute %Q{
|
|
30
|
+
CREATE TABLE bar_models (
|
|
31
|
+
id integer,
|
|
32
|
+
foo_model_id integer,
|
|
33
|
+
string_field text,
|
|
34
|
+
primary key(id)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ActiveRecord::Base.connection.execute %Q{ DROP TABLE IF EXISTS baz_models }
|
|
39
|
+
ActiveRecord::Base.connection.execute %Q{
|
|
40
|
+
CREATE TABLE baz_models (
|
|
41
|
+
id integer,
|
|
42
|
+
foo_model_id integer,
|
|
43
|
+
string_field text,
|
|
44
|
+
primary key(id)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ActiveRecord::Base.connection.execute %Q{ DROP TABLE IF EXISTS bam_models }
|
|
49
|
+
ActiveRecord::Base.connection.execute %Q{
|
|
50
|
+
CREATE TABLE bam_models (
|
|
51
|
+
id integer,
|
|
52
|
+
bar_model_id integer,
|
|
53
|
+
string_field text,
|
|
54
|
+
primary key(id)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
25
58
|
class FooModel < ActiveRecord::Base
|
|
26
59
|
include Httpsql
|
|
27
|
-
|
|
60
|
+
has_one :bar_model
|
|
61
|
+
has_many :baz_models
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class BarModel < ActiveRecord::Base
|
|
65
|
+
include Httpsql
|
|
66
|
+
belongs_to :foo_model
|
|
67
|
+
has_many :bam_models
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class BazModel < ActiveRecord::Base
|
|
71
|
+
include Httpsql
|
|
72
|
+
belongs_to :foo_model
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class BamModel < ActiveRecord::Base
|
|
76
|
+
include Httpsql
|
|
77
|
+
belongs_to :bar_model
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class TestApi < Grape::API
|
|
81
|
+
resource :foo_models do
|
|
82
|
+
desc 'foo models index'
|
|
83
|
+
params do
|
|
84
|
+
FooModel.grape_documentation(self)
|
|
85
|
+
end
|
|
86
|
+
get '/' do
|
|
87
|
+
FooModel.with_params(params)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
resource :baz_models do
|
|
92
|
+
desc 'baz models index'
|
|
93
|
+
params do
|
|
94
|
+
BazModel.grape_documentation(self)
|
|
95
|
+
end
|
|
96
|
+
get '/' do
|
|
97
|
+
BazModel.with_params(params)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
28
101
|
end
|
|
29
102
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: httpsql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -11,24 +11,24 @@ authors:
|
|
|
11
11
|
autorequire:
|
|
12
12
|
bindir: bin
|
|
13
13
|
cert_chain: []
|
|
14
|
-
date: 2013-07-
|
|
14
|
+
date: 2013-07-23 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
16
|
- !ruby/object:Gem::Dependency
|
|
17
17
|
name: activerecord
|
|
18
18
|
requirement: !ruby/object:Gem::Requirement
|
|
19
19
|
none: false
|
|
20
20
|
requirements:
|
|
21
|
-
- -
|
|
21
|
+
- - ~>
|
|
22
22
|
- !ruby/object:Gem::Version
|
|
23
|
-
version: '3.
|
|
23
|
+
version: '3.2'
|
|
24
24
|
type: :runtime
|
|
25
25
|
prerelease: false
|
|
26
26
|
version_requirements: !ruby/object:Gem::Requirement
|
|
27
27
|
none: false
|
|
28
28
|
requirements:
|
|
29
|
-
- -
|
|
29
|
+
- - ~>
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: '3.
|
|
31
|
+
version: '3.2'
|
|
32
32
|
- !ruby/object:Gem::Dependency
|
|
33
33
|
name: arel
|
|
34
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,7 +52,7 @@ dependencies:
|
|
|
52
52
|
requirements:
|
|
53
53
|
- - ! '>='
|
|
54
54
|
- !ruby/object:Gem::Version
|
|
55
|
-
version:
|
|
55
|
+
version: 0.5.0
|
|
56
56
|
type: :runtime
|
|
57
57
|
prerelease: false
|
|
58
58
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -60,7 +60,7 @@ dependencies:
|
|
|
60
60
|
requirements:
|
|
61
61
|
- - ! '>='
|
|
62
62
|
- !ruby/object:Gem::Version
|
|
63
|
-
version:
|
|
63
|
+
version: 0.5.0
|
|
64
64
|
- !ruby/object:Gem::Dependency
|
|
65
65
|
name: bundler
|
|
66
66
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -93,6 +93,38 @@ dependencies:
|
|
|
93
93
|
- - ! '>='
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: 0.5.7
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: minitest
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
none: false
|
|
100
|
+
requirements:
|
|
101
|
+
- - ~>
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '4.2'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
none: false
|
|
108
|
+
requirements:
|
|
109
|
+
- - ~>
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '4.2'
|
|
112
|
+
- !ruby/object:Gem::Dependency
|
|
113
|
+
name: pry-nav
|
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
|
115
|
+
none: false
|
|
116
|
+
requirements:
|
|
117
|
+
- - ! '>='
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '0'
|
|
120
|
+
type: :development
|
|
121
|
+
prerelease: false
|
|
122
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
123
|
+
none: false
|
|
124
|
+
requirements:
|
|
125
|
+
- - ! '>='
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: '0'
|
|
96
128
|
- !ruby/object:Gem::Dependency
|
|
97
129
|
name: rake
|
|
98
130
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -151,6 +183,7 @@ executables: []
|
|
|
151
183
|
extensions: []
|
|
152
184
|
extra_rdoc_files: []
|
|
153
185
|
files:
|
|
186
|
+
- .coveralls.yml
|
|
154
187
|
- .gitignore
|
|
155
188
|
- .travis.yml
|
|
156
189
|
- Gemfile
|