httpsql 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/httpsql.png)](http://badge.fury.io/rb/httpsql)
|
4
|
-
[![Build Status](https://travis-ci.org/Adaptly/httpsql.png)](https://travis-ci.org/Adaptly/httpsql)
|
4
|
+
[![Build Status](https://travis-ci.org/Adaptly/httpsql.png?branch=master)](https://travis-ci.org/Adaptly/httpsql)
|
5
5
|
[![Code Climate](https://codeclimate.com/github/Adaptly/httpsql.png)](https://codeclimate.com/github/Adaptly/httpsql)
|
6
6
|
[![Dependency Status](https://gemnasium.com/Adaptly/httpsql.png)](https://gemnasium.com/Adaptly/httpsql)
|
7
7
|
[![Coverage Status](https://coveralls.io/repos/Adaptly/httpsql/badge.png)](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
|