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 ADDED
@@ -0,0 +1 @@
1
+ repo_token: mUabjcN9fjNpphNtEUHYDJtE1VzEa8p12
data/.gitignore CHANGED
@@ -15,3 +15,6 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ *.sw[a-z]
19
+ *~
20
+ *.orig
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 Record](http://api.rubyonrails.org/classes/ActiveRecord/Base.html)
10
- models exposed by [grape](https://github.com/intridea/grape). Once the module is
11
- included, a given model can respond directly to query params passed to it, using
12
- `where_params_eq`. You can also constrain the fields returned by the model,
13
- using the `fields` query parameter.
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 (`#route_params`).
25
+ into your existing documentation (`#grape_documentation`).
21
26
 
22
- Httpsql reserves one parameter, access_token. If your model has a field called
23
- access_token, you'll need to rename it.
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
- ### model.rb
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
- optional_params: Widget.route_params
68
- }
87
+ desc 'Get all widgets'
88
+ params do
89
+ Widget.grape_documentation(self)
90
+ end
69
91
  get '/' do
70
- present Widget.where_params_eq(params)
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&fields[]=id&fields[]=int_field'
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
 
@@ -1,6 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
  gem "activerecord", "~> 3.1.0"
3
- gem "grape"
3
+ gem "grape", ">= 0.5.0"
4
4
  gem "bundler", "~> 1.3"
5
5
  gem "coveralls", ">= 0.5.7"
6
6
  gem "rake"
@@ -1,6 +1,6 @@
1
1
  source "https://rubygems.org"
2
2
  gem "activerecord", "~> 3.2.0"
3
- gem "grape"
3
+ gem "grape", ">= 0.5.0"
4
4
  gem "bundler", "~> 1.3"
5
5
  gem "coveralls", ">= 0.5.7"
6
6
  gem "rake"
@@ -1,6 +1,7 @@
1
1
  source "https://rubygems.org"
2
+ gem "minitest", "~> 4.2"
2
3
  gem "activerecord", "~> 4.0.0"
3
- gem "grape"
4
+ gem "grape", ">= 0.5.0"
4
5
  gem "bundler", "~> 1.3"
5
6
  gem "coveralls", ">= 0.5.7"
6
7
  gem "rake"
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", ">= 3.1"
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
- def where_params_eq(params={})
10
- fields = params[:field] || []
11
- conds = []
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
- valid_params(params).each do |k,v|
14
- (k, m) = k.to_s.split('.')
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
- if m
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
- def route_params
40
- columns.inject({}) do |m,c|
41
- m[c.name] = {
42
- type: c.sql_type,
43
- desc: c.name,
44
- primary: c.primary
45
- }
46
- m
47
- end.merge "field" => {
48
- type: 'array',
49
- desc: 'select fields',
50
- primary: false
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 valid_params(params)
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
 
@@ -1,3 +1,3 @@
1
1
  module Httpsql
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
data/test/httpsql_test.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'test_helper'
2
2
 
3
- def generate_models
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
- it 'selects a model\'s columns from a given hash' do
19
- ret = FooModel.send(: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)
20
- 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')
21
- end
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
- it 'selects all models' do
24
- models = generate_models
25
- FooModel.where_params_eq({}).must_equal models
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
- it 'selects a specified array of models' do
29
- models = generate_models
30
- FooModel.where_params_eq("int_field" => [0, 1]).must_equal models[0..1]
31
- end
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
- it 'selects a model, using eq' do
34
- models = generate_models
35
- FooModel.where_params_eq("int_field.eq" => 0).must_equal [models[0]]
36
- end
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
- it 'selects models, using not_eq' do
39
- models = generate_models
40
- FooModel.where_params_eq("int_field.not_eq" => 0).must_equal models[1..-1]
41
- end
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
- it 'selects a model, using matches' do
44
- models = generate_models
45
- FooModel.where_params_eq("string_field.matches" => "%hre%").must_equal [models[3]]
46
- end
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
- it 'selects models, using does_not_match' do
49
- models = generate_models
50
- FooModel.where_params_eq("string_field.does_not_match" => "%ero").must_equal models[1..-1]
51
- end
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
- it 'selects models, using gteq' do
58
- models = generate_models
59
- FooModel.where_params_eq("int_field.gteq" => 2).must_equal models[2..-1]
60
- end
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
- it 'select models, using lt' do
63
- models = generate_models
64
- FooModel.where_params_eq("int_field.lt" => 1).must_equal [models[0]]
65
- end
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
- it 'selects models, using lteq' do
68
- models = generate_models
69
- FooModel.where_params_eq("int_field.lteq" => 2).must_equal models[0..2]
70
- end
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
- it 'selects models, using two ARel methods' do
73
- models = generate_models
74
- FooModel.where_params_eq("int_field.gteq" => 1, "id.gt" => 4).must_equal [models[4]]
75
- end
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
- it 'ignores access_token' do
78
- models = generate_models
79
- FooModel.where_params_eq("access_token" => "111").must_equal models
80
- end
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
- it 'ignores access_token dot notation' do
83
- models = generate_models
84
- FooModel.where_params_eq("access_token.eq" => "111").must_equal models
85
- end
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
- it 'selects a model with specified fields' do
88
- generate_models
89
- model = FooModel.select([:int_field, :id]).where(int_field: 0)
90
- FooModel.where_params_eq("int_field.eq" => 0, field: [:int_field, :id]).must_equal model
91
- end
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
- it 'selects the maximum value for the specified field' do
100
- models = generate_models
101
- expected = models.collect(&:int_field).max
102
- FooModel.where_params_eq("int_field.maximum" => nil).first.int_field.must_equal expected
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
- it 'selects the minimum value for the specified field' do
106
- models = generate_models
107
- expected = models.collect(&:int_field).min
108
- FooModel.where_params_eq("int_field.minimum" => nil).first.int_field.must_equal expected
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
- it 'generates the specified sql for rounding' do
112
- models = generate_models
113
- expected = models.collect(&:dec_field).map{|v| v.round.to_f}
114
- FooModel.where_params_eq("dec_field.round" => "1").collect(&:dec_field).map{|v| v.to_f}.must_equal expected
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
- it 'generates the correct documentation' do
118
- FooModel.route_params.must_equal({
119
- "id" => {:type => "integer", :desc => "id", :primary => true},
120
- "int_field" => {:type => "integer", :desc => "int_field", :primary => false},
121
- "dec_field" => {:type => "decimal", :desc => "dec_field", :primary => false},
122
- "string_field" => {:type => "text", :desc => "string_field", :primary => false},
123
- "access_token" => {:type => "text", :desc => "access_token", :primary => false},
124
- "created_at" => {:type => "text", :desc => "created_at", :primary => false},
125
- "updated_at" => {:type => "text", :desc => "updated_at", :primary => false},
126
- "field" => {:type => "array", :desc => "select fields", :primary => false}}
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
- #attr_accessible :int_field, :dec_field, :string_field, :access_token
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.1.2
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-18 00:00:00.000000000 Z
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.1'
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.1'
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: '0'
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: '0'
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